diff --git a/.editorconfig b/.editorconfig index 762d29c8..a1324524 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,16 @@ -[*.{kt, kts, java, xml, gradle, md}] +[*.{kt,kts,java,xml,gradle,md}] + +# General rules disabled_rules = import-ordering, filename wildcard_import_limit = 999 + +# Kotlin rules ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999 ij_java_class_count_to_use_import_on_demand = 999 -max_line_length = 120 + +# Line rules +max_line_length = 160 +end_of_line = lf +insert_final_newline = true +ij_any_method_annotation_wrap = split_into_lines diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..032e9a9c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# app/features/src/debug/**/*.png filter=lfs diff=lfs merge=lfs -text +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3a82c9ea..a78aeedf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,14 @@ .cxx repo /artifacts - +ci/.idea +ci/env +app/android/r8 +.kotlin/ +signing.properties +erp-release-keystore-huawei.jks +erp-release-keystore-play.jks +requirements-report.html Jenkinsfile Nightly.Jenkinsfile Archive.Jenkinsfile @@ -21,14 +28,27 @@ DependencyReport.Jenkinsfile EspressoTest.Jenkinsfile Release.Jenkinsfile Multibranch.Jenkinsfile -ci ci-overrides.properties nexus-init.gradle.kts -documentation-internal -android/src/androidTest technical_requirements_report.html -keystore CODEOWNERS local.cliactions.yaml +**/virtual_smartcards.json +2021_s.csv +2024_s.csv +mapping.txt +.aiexclude +sonar-project.properties +.snyk +keystore jenkinsfiles +desktop +smartcard-wrapper +common/src/desktopMain +app/test-actions +documentation-internal +app/android/src/androidTest +ci +requirements +gutachter diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..ba4cd8b2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Calls for violence, vilification and advertising +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at OSPO@gematik.de. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a7060fa6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Welcome to the gematik contributing guide + +Thank you for investing your time in contributing to our projects! + +Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. + +In this guide you will get an overview how you can contribute to our projects by opening an issue, creating, reviewing and merging a pull request. + +Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly. + +## Reporting a security vulnerability + +Please do not report vulnerabilities and security incidents as GitHub issues. Please contact us by sending an E-Mail to TODO or report them using the contact form at https://fachportal.gematik.de/kontaktformular. + +## New contributor guide + +To get an overview of the project, read the [README](./README.md). + +## Getting started + +### Issues + +#### Create a new issue + +If you spot a problem or have a feature request, search if an issue already exists. +If a related issue doesn't exist, you can open a new issue. + +#### Solve an issue + +Scan through our existing issues to find one that interests you. If you find an issue to work on, you are welcome to open a PR with a fix. + +### Coding Style + +gematik projects follow the kotlin style guide conventions, see [kotlin style guide android](https://developer.android.com/kotlin/style-guide) or [kotlin lang style guide](https://kotlinlang.org/docs/coding-conventions.html). Please follow them when working on your contributions. + +### Code Coverage, Sonars, OWASP, Code format, etc. + +- CodeFormat & Style: Please check your code with Ktlint and Detekt. commands: "./gradlew ktlintformat" & "./gradlew detekt" +- Code Coverage: If you add UseCases or ViewModels please add corresponding UnitTests as well. +- Code Coverage: If you add or change UI make sure these changes are covered by screenshotstests "./gradlew verifyPaparazziDebug". + + +### Commit your update + +Commit the changes once you are happy with them. + +### Pull Request Process + +- When you're finished with the changes, create a pull request, also known as a PR. +- Fill the pull request template so that we can review your PR. This template helps reviewers to understand your changes as well as the purpose of your pull request. +- Don't forget to [link the PR to the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. +- Once you submit your PR, a project team member will review your proposal. We may ask questions or request additional information. +- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) + or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. +- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). +- If your pull request is approved by our developers, we may merge it into the project. + +### Your PR is merged! + +Congratulations: The gematik team thanks you. + +Once your PR is merged, your contributions will be publicly visible on the [gematik github page](https://github.com/gematik/). diff --git a/LICENSE.md b/LICENSE.md index 9348db2a..bfac47dd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,14 +1,14 @@ +# License Copyright (c) 2024 gematik GmbH - Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent versions of the EUPL (the Licence); You may not use this work except in compliance with the Licence. You may obtain a copy of the Licence at: - https://joinup.ec.europa.eu/software/page/eupl +https://joinup.ec.europa.eu/software/page/eupl Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and -limitations under the Licence. \ No newline at end of file +limitations under the Licence. diff --git a/README.md b/README.md index 28c18ce5..6641e65c 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,57 @@ -# E-Rezept App -## Introduction - +gematik logo
+ +# E-Rezept App + +## Table Of Contents + +- [About The Project](#about-the-project) + - [Release Notes](#release-notes) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Structure](#structure) + - [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [Security And Privacy](#security-and-privacy) +- [License](#license) +- [Contact](#contact) +- [Additional Links And Sourcecode](#additional-links-and-sourcecode) + +## About The Project Prescriptions for medicines that are only available in pharmacies can be issued as electronic prescriptions (e-prescriptions resp. E-Rezepte) for people with public health insurance from 1 July 2021. The official gematik E-Rezept App (electronic prescription app) is available to receive and redeem prescriptions digitally. Anyone can download the app for free: [![Download E-Rezept on the App Store](https://user-images.githubusercontent.com/52454541/126137060-cb8c7ceb-6a72-423d-9079-f3e1a98b2638.png)](https://apps.apple.com/de/app/das-e-rezept/id1511792179)[![Download E-Rezept on the PlayStore](https://user-images.githubusercontent.com/52454541/126138350-a52e1d84-1588-4e8a-86df-189ee4df8bc8.png)](https://play.google.com/store/apps/details?id=de.gematik.ti.erp.app)[![Download E-Rezept on the App Gallery](https://user-images.githubusercontent.com/52454541/126158983-15d73f12-36c6-41ce-8de5-29d10baaed04.png)](https://appgallery.huawei.com/#/app/C104463531) -and login with the health card of the public health insurance. In July 2021, the e-prescription started with a test phase, initially in the focus region Berlin-Brandenburg. The nationwide rollout started three month later in September 2022. +or as an apk here in GitHub: [Releases](https://github.com/gematik/E-Rezept-App-Android/releases) + +Login is possible with the health card or the app of the users public health insurance company. In July 2021, the e-prescription started with a test phase, initially in the focus region Berlin-Brandenburg. The nationwide rollout started three month later in September 2022. The e-prescriptions are stored in the telematics infrastructure, for which gematik is responsible. Visit our [FAQ page](https://www.das-e-rezept-fuer-deutschland.de/faq) for more information about the e-prescription. -### Support & Feedback - -For endusers and insurant: - -[![E-Rezept Webseite](https://img.shields.io/badge/web-E%20Rezept%20Webseite-green?logo=web.ru&style=flat-square&logoColor=white)](https://www.das-e-rezept-fuer-deutschland.de/) -[![eMail E-Rezept](https://img.shields.io/badge/email-E%20Rezept%20team-green?logo=mail.ru&style=flat-square&logoColor=white)](mailto:app-feedback@gematik.de) -[![E-Rezept Support Telephone](https://img.shields.io/badge/phone-E%20Rezept%20Service-green?logo=phone.ru&style=flat-square&logoColor=white)](tel:+498002773777) - -Members of the health-industry with functional questions - -[![eMail E-Rezept Team](https://img.shields.io/badge/web-E%20Rezept%20Industrie-green?logo=web.ru&style=flat-square&logoColor=white)](https://www.gematik.de/hilfe-kontakt/hersteller/) - -IT specialists - -[![eMail E-Rezept Fachportal](https://img.shields.io/badge/web-E%20Rezept%20Fachportal-green?logo=web.ru&style=flat-square&logoColor=white)](https://fachportal.gematik.de/anwendungen/elektronisches-rezept) -[![eMail E-Rezept Team](https://img.shields.io/badge/email-E%20Rezept%20team-green?logo=mail.ru&style=flat-square&logoColor=white)](mailto:app-feedback@gematik.de) - -### Data Privacy +### Release Notes +See [ReleaseNotes.md](./ReleaseNotes.md) for all information regarding the (newest) releases. -You can find the privacy policy for the app at: [https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz](https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz) +## Getting Started +This section provides instructions on how to get started with the project, +including setting up the development environment and building the application. -### Contributors +### Prerequisites +Before you can build and run the application, ensure you have the following prerequisites installed on your system: -We plan to enable contribution to the E-Rezept App in the near future. +- **Android Studio:** The official IDE for Android app development. +- **Kotlin:** The primary programming language used in this project. Android Studio comes bundled with Kotlin support. +- **Git:** A distributed version control system used to manage the project's source code. -### Licensing +**Getting the Project Code** -The E-Rezept App is licensed under the European Union Public Licence (EUPL); every use of the E-Rezept App Sourcecode must be in compliance with the EUPL. - -You will find more details about the EUPL here: [https://joinup.ec.europa.eu/collection/eupl](https://joinup.ec.europa.eu/collection/eupl) - -Unless required by applicable law or agreed to in writing, software distributed under the EUPL is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the EUPL for the specific language governing permissions and limitations under the License. - -## Development - -### Getting started - -To get started, build one of the \*Pu\* variants. Currently, the Google and Huawei variants differ only in configuration. The code is identical. This is likely to change soon. - -This repository is an [Kotlin Multiplatform Project](https://kotlinlang.org/docs/multiplatform.html) unifying the upcoming E-Rezept App for desktop and the Android App. +To begin, clone the project's repository from GitHub using the following command in your terminal: +"git clone https://github.com/gematik/E-Rezept-App-Android.git" or by using the version control tool of Android Studio. ### Structure - +The following graphic provides an overview over the more important parts of the kotlin multiplatform project: ```text |-- app | `-- android @@ -77,52 +74,61 @@ This repository is an [Kotlin Multiplatform Project](https://kotlinlang.org/docs | |-- androidTest | |-- main | `-- test -| `-- shared-test +| `-- test-actions | `-- src | |-- main |-- common | `-- src | |-- androidMain -| |-- androidTest | |-- commonMain | |-- commonTest -| |-- desktopMain -| `-- desktopTest -|-- desktop -| `-- src -| |-- jvmMain -| `-- jvmTest -`-- plugins - `-- dependencies +`-- ui-components + `-- src ``` -`plugins/dependencies` is a [composed build](https://docs.gradle.org/current/userguide/composite_builds.html) required by any of the other modules (android, common and desktop) managing the dependencies in one place. +- **app/android/src/main:** includes build parameters for the android app. +- **app/android-mock/src/main:** includes build parameters for the android app-mock (mostly used for testing). +- **app/features/src/main:** includes the core logic of the android app. Including Screens, UseCases and Navigation. +- **app/demo-mode/src/main:** includes the core logic of the demo-mode of the android app. Including all overwritten Datasources and UseCases +- **common/src/commonMain:** includes the general logic of app. Most important the localDatasource and it's entities. -The `gradle.properties` file contains all pre-defined properties required to communicate with the FD (**F**ach**D**ienst), IDP (**ID**entity **P**rovider) and the pharmacy lookup service. -Unfortunately the actual values are not meant to be public. +### Installation +To create and install an unsigned debug.apk on your connected android device, run: +- "./gradlew :app:android:assembleGoogleTuInternalDebug" in your terminal. + - This will create an apk in the following path "app/android/build/outputs/apk/googleTuInternal/debug", which you can then install on your device + - run "adb install app/android/build/outputs/apk/googleTuInternal/debug/android-googleTuInternal-debug.apk" +- or click on the green "run"-Button in the top right of android studio. -### Android +## Usage +The installed debug.apk can't communicate to connected servers. It's purpose is for local testing only. To show some data, you can enter the demo-mode via the settings. +If you want or need an active app, please download a live version from the sources mentioned in the [About The Project](#about-the-project) section. -To build the Android App choose one variant (e.g. `gradle :android:assembleGooglePuExternalDebug -Pbuildkonfig.flavor=googlePuExternal`): +## Contributing +See [Contributing.md](./CONTRIBUTING.md) for all information regarding the the contributing process in this project. -```shell -gradle :android:assemble(Google|Huawei)Pu(External|Internal)(Debug|Release) -Pbuildkonfig.flavor=(google|huawei)Pu(External|Internal) -``` +## Security And Privacy +See [Security.md](./SECURITY.md) for all information regarding the used security and privacy guidelines in this project. -*Note: Currently the android build variant is derived from the `buildkonfig.flavor` property.* +## License +See [License.md](./LICENSE.md) for all information regarding the used license. -#### APK +## Contact +For endusers and insurant: -The resulting `.apk` can be found in e.g. `app/android/build/outputs/apk/googlePuExternal/debug/`. +[![E-Rezept Webseite](https://img.shields.io/badge/web-E%20Rezept%20Webseite-green?logo=web.ru&style=flat-square&logoColor=white)](https://www.das-e-rezept-fuer-deutschland.de/) +[![eMail E-Rezept](https://img.shields.io/badge/email-E%20Rezept%20team-green?logo=mail.ru&style=flat-square&logoColor=white)](mailto:app-feedback@gematik.de) +[![E-Rezept Support Telephone](https://img.shields.io/badge/phone-E%20Rezept%20Service-green?logo=phone.ru&style=flat-square&logoColor=white)](tel:+498002773777) -Additionally, you can find the latest apk [here](https://github.com/gematik/E-Rezept-App-Android/releases/latest) +Members of the health-industry with functional questions -#### Visualize Test Tags +[![eMail E-Rezept Team](https://img.shields.io/badge/web-E%20Rezept%20Industrie-green?logo=web.ru&style=flat-square&logoColor=white)](https://www.gematik.de/hilfe-kontakt/hersteller/) -See [Visualize Test Tags](documentation/test-tags.md) +IT specialists +[![eMail E-Rezept Fachportal](https://img.shields.io/badge/web-E%20Rezept%20Fachportal-green?logo=web.ru&style=flat-square&logoColor=white)](https://fachportal.gematik.de/anwendungen/elektronisches-rezept) +[![eMail E-Rezept Team](https://img.shields.io/badge/email-E%20Rezept%20team-green?logo=mail.ru&style=flat-square&logoColor=white)](mailto:app-feedback@gematik.de) -### Links Sourcecode +### Additional Links And Sourcecode - [E-Rezept iOS implementation](https://github.com/gematik/E-Rezept-App-iOS) - Reference implementation of the [IDP (**ID**entity **P**rovider)](https://github.com/gematik/ref-idp-server) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1b2dd8a3..e1907111 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,7 +1,49 @@ +# Release 1.26.0 +- added: link to organ donation register in settings +- added: in-app notification about the latest changes +- added: option to have both appPassword and device credentials as login method +- changed: accessibility overhaul on many screens +- Bug fixes +- infrastructure/ project improvements +- UX improvements + +# Release 1.25.1 +- added: saving the credentials of the user when logging in with gid +- added: no internet hint +- reworked: app-login +- Bug fixes +- infrastructure/ project improvements +- UX improvements + +# Release 1.24.0 +- Bug fixes +- infrastructure/ project improvements +- UX improvements + +# Release 1.23.0 +- added: help-section for GID usage +- added: in-app language change for android 13 and higher, lower versions are getting redirected to android settings +- added: profile picture enhancements: stickers, emojis, bitmojis +- Bug fixes +- infrastructure/ project improvements +- UX improvements + +# Release 1.22.0 +- Saving credentials possible for all devices with hardware backed keystore +- Redeem from detail view +- Refactoring of pharmacy feature, redeem feature +- Bug fixes +- Url updates +- UX improvements + +# Release 1.21.0 +- skipped + # Release 1.20.0 -- Added multi-language support for privacy policy +- Refactor of egk card feature +- Partial refactoring of pharmacy feature +- Add multi language support for privacy policy - Update health insurance contacts -- Optimized performance - Bug fixes # Release 1.19.1 diff --git a/app/android-mock/build.gradle.kts b/app/android-mock/build.gradle.kts index 6fa67b70..c7d408a1 100644 --- a/app/android-mock/build.gradle.kts +++ b/app/android-mock/build.gradle.kts @@ -1,74 +1,33 @@ -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import de.gematik.ti.erp.overriding -import org.owasp.dependencycheck.reporting.ReportGenerator.Format +@file:Suppress("VariableNaming", "PropertyName", "UnusedPrivateProperty") -// TODO: Duplicate of android build.gradle, make this into one -plugins { - id("com.android.application") - kotlin("android") - id("org.jetbrains.compose") - id("io.realm.kotlin") - kotlin("plugin.serialization") - id("org.owasp.dependencycheck") - id("com.jaredsburrows.license") - id("de.gematik.ti.erp.dependencies") - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("de.gematik.ti.erp.technical-requirements") -} - -val VERSION_CODE: String by overriding() -val VERSION_NAME: String by overriding() -val TEST_INSTRUMENTATION_ORCHESTRATOR: String? by project +import de.gematik.ti.erp.app.plugins.dependencies.overrides +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin -afterEvaluate { - val taskRegEx = """assemble(Google|Huawei)(PuExternalDebug|PuExternalRelease)""".toRegex() - tasks.forEach { task -> - taskRegEx.matchEntire(task.name)?.let { - val (_, version, flavor) = it.groupValues - task.dependsOn(tasks.getByName("license${version}${flavor}Report")) - } - } +plugins { + id("base-android-application") + id("de.gematik.ti.erp.names") + id("de.gematik.ti.erp.dependency-overrides") } -licenseReport { - generateCsvReport = false - generateHtmlReport = false - generateJsonReport = true - copyJsonReportToAssets = true -} +val VERSION_CODE: String by overrides() +val VERSION_NAME: String by overrides() +val gematik = AppDependencyNamesPlugin() android { - namespace = "de.gematik.ti.erp.app.mock" + namespace = gematik.moduleName("mock") defaultConfig { - applicationId = "de.gematik.ti.erp.app.mock" + applicationId = gematik.idName("mock") versionCode = VERSION_CODE.toInt() versionName = VERSION_NAME - testApplicationId = "de.gematik.ti.erp.app" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testApplicationId = gematik.moduleName("mock.test") + // Check if MAPS_API_KEY is defined, otherwise provide a default value + val mapsApiKey = project.findProperty("MAPS_API_KEY") ?: "DEFAULT_PLACEHOLDER_KEY" + manifestPlaceholders["MAPS_API_KEY"] = mapsApiKey } - androidResources { - noCompress("srt", "csv", "json") + generateLocaleConfig = true } - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - dependencyCheck { - analyzers.assemblyEnabled = false - suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" - formats = listOf(Format.HTML, Format.XML) - scanConfigurations = configurations.filter { - it.name.startsWith("api") || - it.name.startsWith("implementation") || - it.name.startsWith("kapt") - }.map { - it.name - } - } - buildTypes { val release by getting { resValue("string", "app_label", "E-Rezept Mock") @@ -78,135 +37,30 @@ android { resValue("string", "app_label", "E-Rezept Mock") versionNameSuffix = "-debug" } - create("minifiedDebug") { + create(gematik.minifiedDebug) { initWith(debug) } } - - packagingOptions { - resources { - excludes += "META-INF/**" - // for JNA and JNA-platform - excludes += "META-INF/AL2.0" - excludes += "META-INF/LGPL2.1" - // for byte-buddy - excludes += "META-INF/licenses/ASM" - pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" - pickFirsts += "win32-x86/attach_hotspot_windows.dll" - } - } } dependencies { - implementation(project(":app:features")) - implementation(project(":app:demo-mode")) - androidTestImplementation(project(":app:shared-test")) - implementation(project(":common")) - testImplementation(project(":common")) - testImplementation(kotlin("test")) - implementation("com.tom-roush:pdfbox-android:2.0.27.0") { - exclude(group = "org.bouncycastle") - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) - } - - inject { - dateTime { - implementation(datetime) - testCompileOnly(datetime) - } - android { - coreLibraryDesugaring(desugaring) - debugImplementation(processPhoenix) - } - androidX { - implementation(appcompat) - implementation(composeNavigation) - implementation(security) - implementation(lifecycleViewmodel) - implementation(lifecycleProcess) - implementation(lifecycleComposeRuntime) - } - dependencyInjection { - compileOnly(kodeinCompose) - androidTestImplementation(kodeinCompose) - } - logging { - implementation(napier) - } - tracking { - implementation(contentSquare) - } - compose { - implementation(runtime) - implementation(foundation) - implementation(uiTooling) - implementation(preview) - } - crypto { - testImplementation(jose4j) - testImplementation(bouncycastleBcprov) - testImplementation(bouncycastleBcpkix) - } - database { - compileOnly(realm) - testCompileOnly(realm) - } - network { - implementation(retrofit) - implementation(retrofit2KotlinXSerialization) - implementation(okhttp3) - implementation(okhttpLogging) - // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). - // Can be removed as soon as Retrofit releases a new version >2.9.0. - implementation(okio) - - androidTestImplementation(okhttp3) - } - playServices { - implementation(appUpdate) - } - serialization { - implementation(kotlinXJson) - } - androidXTest { - testImplementation(archCore) - androidTestImplementation(core) - androidTestImplementation(rules) - androidTestImplementation(junitExt) - androidTestImplementation(runner) - androidTestUtil(orchestrator) - androidTestUtil(services) - // androidTestImplementation(navigation) - androidTestImplementation(espresso) - androidTestImplementation(espressoIntents) - } - tracing { - debugImplementation(tracing) - implementation(tracing) - } - coroutinesTest { - testImplementation(coroutinesTest) - } - composeTest { - androidTestImplementation(ui) - debugImplementation(uiManifest) - androidTestImplementation(junit4) - } - networkTest { - testImplementation(mockWebServer) - } - test { - testImplementation(junit4) - testImplementation(snakeyaml) - testImplementation(json) - testImplementation(mockk) - androidTestImplementation(mockkAndroid) - } - } + implementation(project(gematik.feature)) + implementation(project(gematik.demoMode)) + implementation(project(gematik.uiComponents)) + implementation(project(gematik.multiplatform)) + implementation(libs.tracing) + implementation(libs.bundles.crypto) + implementation(libs.bundles.accompanist) + implementation(libs.bundles.database) + testImplementation(project(gematik.multiplatform)) + androidTestImplementation(project(gematik.testActions)) + androidTestImplementation(project(gematik.testTags)) + debugImplementation(libs.tracing) } -secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) "ci-overrides.properties" else "gradle.properties" +// todo: check if this affects the ui-tests +configurations.all { + resolutionStrategy { + force("com.google.protobuf:protobuf-java:4.28.2") + } } diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt deleted file mode 100644 index 224a2801..00000000 --- a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import de.gematik.ti.erp.app.sharedtest.testresources.actions.PharmacyScreenAction - -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PharmacyUITest { - @get:Rule - val composeRule = createAndroidComposeRule() - - private val actions = PharmacyScreenAction(composeRule) - - @Test - fun pickupServiceSuccessTest() { - actions.pickupServiceSuccessTest() - } - - @Test - fun courierDeliverySuccessTest() { - actions.courierDeliverySuccessTest() - } - - @Test - fun pickupServiceMailDeliverySuccessTest() { - actions.pickupServiceMailDeliverySuccessTest() - } - - @Test - fun mailDeliverySuccessTest() { - actions.mailDeliverySuccessTest() - } - - @Test - fun pickupServiceCourierSuccessTest() { - actions.pickupServiceCourierSuccessTest() - } - - @Test - fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { - actions.pickupServiceMailDeliveryCourierDeliverySuccessTest() - } - - @Test - fun mailDeliveryCourierDeliverySuccessTest() { - actions.mailDeliveryCourierDeliverySuccessTest() - } - - @Test - fun pickupServiceFailTest() { - actions.pickupServiceFailTest() - } - - @Test - fun courierDeliveryFailTest() { - actions.courierDeliveryFailTest() - } - - @Test - fun mailDeliveryFailTest() { - actions.mailDeliveryFailTest() - } - - @Test - fun pickupServiceMailDeliveryCourierDeliveryFailTest() { - actions.pickupServiceMailDeliveryCourierDeliveryFailTest() - } - - @Test - fun pickupServiceMailDeliveryFailTest() { - actions.pickupServiceMailDeliveryFailTest() - } - -} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/UiTest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/UiTest.kt new file mode 100644 index 00000000..4ab0bc6b --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/UiTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app + +import android.content.Context +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.runner.RunWith + +@Suppress("UnnecessaryAbstractClass") +@RunWith(AndroidJUnit4::class) +abstract class UiTest { + private val application = ApplicationProvider.getApplicationContext() as MockErezeptApp + + @get:Rule + val composeRule = createAndroidComposeRule() + + val context: Context = application.applicationContext +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/components/InjectionConfig.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/components/InjectionConfig.kt new file mode 100644 index 00000000..2373f4ba --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/components/InjectionConfig.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.components + +import android.content.Context +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.rules.ActivityScenarioRule +import de.gematik.ti.erp.app.MockMainActivity +import de.gematik.ti.erp.app.config.TestScenario +import de.gematik.ti.erp.app.config.injectionConfig + +/** + * Every scenario is injected as an intent to start the mock app for that test + */ + +fun AndroidComposeTestRule, MockMainActivity>.injectConfig( + applicationContext: Context, + config: TestScenario +): ActivityScenario = activityRule.scenario.onActivity { + it.injectionConfig(applicationContext, config) +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/onboarding/OnboardingTests.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/onboarding/OnboardingTests.kt new file mode 100644 index 00000000..8a808d5b --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/onboarding/OnboardingTests.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.UiTest +import de.gematik.ti.erp.app.components.injectConfig +import de.gematik.ti.erp.app.config.TestScenario +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class OnboardingTests : UiTest() { + + @Test + fun onboardingNotDoneTest() { + runTest { + composeRule.injectConfig( + applicationContext = context, + config = TestScenario(isOnboardingDone = false) + ) + composeRule.apply { + onNodeWithTag(TestTag.Onboarding.WelcomeScreen) + .assertIsDisplayed() + } + } + } + + @Test + fun onboardingDoneTest() { + runTest { + composeRule.injectConfig( + applicationContext = context, + config = TestScenario(isOnboardingDone = true) + ) + composeRule.apply { + onNodeWithTag("password_prompt_password_field") + .assertIsDisplayed() + .performTextInput("password") + onNodeWithTag("password_prompt") + .assertIsDisplayed() + .performClick() + } + } + } +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyForUserNotLoggedInTest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyForUserNotLoggedInTest.kt new file mode 100644 index 00000000..88609841 --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyForUserNotLoggedInTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy + +import de.gematik.ti.erp.app.UiTest +import de.gematik.ti.erp.app.testactions.actions.pharmacy.PharmacyScreenAction +import org.junit.Test + +class PharmacyForUserNotLoggedInTest : UiTest() { + + private val actions = PharmacyScreenAction(composeRule) + + @Test + fun pickupServiceSuccessTest() { + actions.pickupServiceSuccessTest() + } + + @Test + fun courierDeliverySuccessTest() { + actions.courierDeliverySuccessTest() + } + + @Test + fun pickupServiceMailDeliverySuccessTest() { + actions.pickupServiceMailDeliverySuccessTest() + } + + @Test + fun mailDeliverySuccessTest() { + actions.mailDeliverySuccessTest() + } + + @Test + fun pickupServiceCourierSuccessTest() { + actions.pickupServiceCourierSuccessTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { + actions.pickupServiceMailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun mailDeliveryCourierDeliverySuccessTest() { + actions.mailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun pickupServiceFailTest() { + actions.pickupServiceFailTest() + } + + @Test + fun courierDeliveryFailTest() { + actions.courierDeliveryFailTest() + } + + @Test + fun mailDeliveryFailTest() { + actions.mailDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliveryFailTest() { + actions.pickupServiceMailDeliveryCourierDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryFailTest() { + actions.pickupServiceMailDeliveryFailTest() + } +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/prescription/SubstitutionMedicationTest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/prescription/SubstitutionMedicationTest.kt new file mode 100644 index 00000000..f99addf1 --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/prescription/SubstitutionMedicationTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription + +import de.gematik.ti.erp.app.UiTest +import de.gematik.ti.erp.app.testactions.actions.prescription.PrescriptionScreenAction +import org.junit.Before +import org.junit.Test + +class SubstitutionMedicationTest : UiTest() { + + private val actions = PrescriptionScreenAction(composeRule) + + @Before + fun setup() { + actions.skipOnboarding() + } + + /** + * When substitution medication available + * */ + @Test + fun substituteMedicationSuccessTest() { + actions.substituteMedicationSuccessTest() + } + + /** + * When substitution medication unavailable + * */ + @Test + fun substituteMedicationFailTest() { + actions.substituteMedicationFailTest() + } +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/profile/ProfileEditTest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/profile/ProfileEditTest.kt new file mode 100644 index 00000000..8eb40a18 --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/profile/ProfileEditTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profile + +import de.gematik.ti.erp.app.UiTest +import de.gematik.ti.erp.app.testactions.actions.profile.ProfileMainScreenAction +import org.junit.Before +import org.junit.Test + +class ProfileEditTest : UiTest() { + + private val actions = ProfileMainScreenAction(composeRule) + + @Before + fun setup() { + actions.skipOnboarding() + } + + /** + * change profile image as Symbols + * */ + @Test + fun symbolsAsProfileImage() { + actions.symbolsAsProfileImage() + } + + /** + * change profile image + * */ + @Test + fun changeProfileImageTest() { + actions.changeProfileImageTest() + } + + /** + * check avatar images + * */ + @Test + fun checkAvatarImageTest() { + actions.checkAvatarImageTest() + } + + /** + * Delete profile picture + * */ + @Test + fun deleteAvatarTest() { + actions.deleteAvatarTest() + } + + /** + * profile dialog text check + * */ + @Test + fun profileDialogTextTest() { + actions.profileDialogTextTest() + } +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/settings/ChangeAppLanguageTest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/settings/ChangeAppLanguageTest.kt new file mode 100644 index 00000000..e6d3c4b5 --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/settings/ChangeAppLanguageTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings + +import de.gematik.ti.erp.app.UiTest +import de.gematik.ti.erp.app.testactions.actions.settings.SettingsScreenAction +import org.junit.Before +import org.junit.Test + +class ChangeAppLanguageTest : UiTest() { + + private val actions = SettingsScreenAction(composeRule) + + @Before + fun setup() { + actions.skipOnboarding() + } + + /** + * check app languages exist test + * */ + @Test + fun checkAllLanguageExistTest() { + actions.checkAllLanguageExistTest() + } + + /** + * change app language test + * */ + @Test + fun changeLanguageTest() { + actions.changeLanguageTest() + } +} diff --git a/app/android-mock/src/main/AndroidManifest.xml b/app/android-mock/src/main/AndroidManifest.xml index 075a4453..ba795d06 100644 --- a/app/android-mock/src/main/AndroidManifest.xml +++ b/app/android-mock/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:required="false" /> diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt deleted file mode 100644 index c28ca8ec..00000000 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * "${GEMATIK_COPYRIGHT_STATEMENT}" - */ - -package de.gematik.ti.erp.app - -import androidx.lifecycle.ProcessLifecycleOwner -import com.contentsquare.android.Contentsquare -import com.tom_roush.pdfbox.android.PDFBoxResourceLoader -import de.gematik.ti.erp.app.di.appModules -import de.gematik.ti.erp.app.di.mockFeatureModule -import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver -import de.gematik.ti.erp.app.userauthentication.observer.ProcessLifecycleObserver -import io.github.aakira.napier.DebugAntilog -import io.github.aakira.napier.Napier -import org.kodein.di.DI -import org.kodein.di.DIAware -import org.kodein.di.android.x.androidXModule -import org.kodein.di.bindSingleton -import org.kodein.di.instance - -class DefaultErezeptMockApp : ErezeptApp(), DIAware { - - override val di by DI.lazy { - import(androidXModule(this@DefaultErezeptMockApp)) - importAll(appModules) - importAll(mockFeatureModule, allowOverride = true) - bindSingleton { InactivityTimeoutObserver(instance(), instance()) } - bindSingleton { ProcessLifecycleObserver(ProcessLifecycleOwner, instance()) } - bindSingleton { VisibleDebugTree() } - } - - private val processLifecycleObserver: ProcessLifecycleObserver by instance() - - private val visibleDebugTree: VisibleDebugTree by instance() - - override fun onCreate() { - super.onCreate() - Napier.base(DebugAntilog()) - Napier.base(visibleDebugTree) - - processLifecycleObserver.observeForInactivity() - - PDFBoxResourceLoader.init(this) - - Contentsquare.start(this) - } -} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockErezeptApp.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockErezeptApp.kt new file mode 100644 index 00000000..ccb6fb8a --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockErezeptApp.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +/* + * "${GEMATIK_COPYRIGHT_STATEMENT}" + */ + +package de.gematik.ti.erp.app + +import androidx.lifecycle.ProcessLifecycleOwner +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader +import de.gematik.ti.erp.app.di.appModules +import de.gematik.ti.erp.app.di.mockFeatureModule +import de.gematik.ti.erp.app.usecase.CreateProfileWhenMissingUseCase +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.userauthentication.observer.ProcessLifecycleObserver +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +class MockErezeptApp : ErezeptApp(), DIAware { + + override var di = DI.lazy { + import(androidXModule(this@MockErezeptApp), allowOverride = true) + importAll(appModules, allowOverride = true) + importAll(mockFeatureModule, allowOverride = true) + bindProvider { CreateProfileWhenMissingUseCase(instance(), instance()) } + bindSingleton { InactivityTimeoutObserver(instance(), instance()) } + bindSingleton { ProcessLifecycleObserver(ProcessLifecycleOwner, instance()) } + bindSingleton { VisibleDebugTree() } + } + + private val processLifecycleObserver: ProcessLifecycleObserver by instance() + + private val visibleDebugTree: VisibleDebugTree by instance() + + // only for mock + private val createProfile: CreateProfileWhenMissingUseCase by instance() + + @OptIn(DelicateCoroutinesApi::class) + override fun onCreate() { + super.onCreate() + Napier.base(DebugAntilog()) + Napier.base(visibleDebugTree) + processLifecycleObserver.observeForInactivity() + + PDFBoxResourceLoader.init(this) + GlobalScope.launch { createProfile.invoke() } + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockMainActivity.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockMainActivity.kt new file mode 100644 index 00000000..b40c6fd8 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/MockMainActivity.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app + +import android.content.Intent +import android.os.Build +import de.gematik.ti.erp.app.appupdate.usecase.CheckVersionUseCase +import de.gematik.ti.erp.app.config.TestConfigKey +import de.gematik.ti.erp.app.config.TestScenario +import de.gematik.ti.erp.app.config.UiTestsIntent +import de.gematik.ti.erp.app.demomode.di.demoModeModule +import de.gematik.ti.erp.app.demomode.di.demoModeOverrides +import de.gematik.ti.erp.app.di.overrides.testScenarioOverrides +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.mocks.settings.OnboardingDoneMockSettingsDataSource +import org.kodein.di.Copy +import org.kodein.di.DI +import org.kodein.di.android.closestDI +import org.kodein.di.android.retainedSubDI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +class MockMainActivity : MainActivity() { + + private var diBuilder: DI.MainBuilder? = null + + override val di by retainedSubDI(closestDI(), copy = Copy.All) { + + fullDescriptionOnError = true + fullContainerTreeOnError = true + diBuilder = this + + // add testScenarioOverrides to the DI + intent?.scenarios(diBuilder) + + import(demoModeModule) + if (isDemoMode()) demoModeOverrides() + if (BuildConfig.DEBUG && BuildKonfig.INTERNAL) debugOverrides() + bindProvider { CheckVersionUseCase(instance()) } + + // domain verification is only available on SDK 31 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + bindProvider { Sdk31DomainVerifier(instance()) } + } else { + bindProvider { OlderSdkDomainVerifier() } + } + } +} + +private fun Intent.scenarios(diMainBuilder: DI.MainBuilder?) { + when (this.action.equals(UiTestsIntent)) { + // can be changed when minSDK = 33 + true -> this.getParcelableExtra(TestConfigKey)?.let { testScenario -> + diMainBuilder?.testScenarioOverrides(testScenario) + } + + else -> OnboardingDoneMockSettingsDataSource() + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/ConfigNames.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/ConfigNames.kt new file mode 100644 index 00000000..0ea54a4c --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/ConfigNames.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.config + +internal const val TestConfigKey = "TestConfig" +internal const val UiTestsIntent = "ui-tests" diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/InjectExtensions.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/InjectExtensions.kt new file mode 100644 index 00000000..2952b1bd --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/InjectExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.config + +import android.content.Context +import android.content.Intent +import de.gematik.ti.erp.app.MockMainActivity + +fun MockMainActivity.injectionConfig( + applicationContext: Context, + config: TestScenario +) { + startActivity( + Intent(applicationContext, MockMainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(TestConfigKey, config) + action = UiTestsIntent + } + ) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/TestScenario.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/TestScenario.kt new file mode 100644 index 00000000..d2aa982d --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/config/TestScenario.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.config + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * This data class is a config file which decides what mocks we need to inject based on the test case + */ +@Parcelize +data class TestScenario( + val isOnboardingDone: Boolean = false +) : Parcelable diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/MockDataSource.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/MockDataSource.kt new file mode 100644 index 00000000..096478e0 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/MockDataSource.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.datasource + +import de.gematik.ti.erp.app.datasource.data.MockPrescriptionInfo +import de.gematik.ti.erp.app.datasource.data.MockProfileInfo.mockProfile01 +import de.gematik.ti.erp.app.model.MockProfileLinkedCommunication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.coroutines.flow.MutableStateFlow + +const val INDEX_OUT_OF_BOUNDS = -1 + +class MockDataSource { +// todo: include pharmacies data source here in future! + + val profiles: MutableStateFlow> = + MutableStateFlow(mutableListOf(mockProfile01)) + + val syncedTasks: MutableStateFlow> = + MutableStateFlow( + mutableListOf( + MockPrescriptionInfo.MockSyncedPrescription.syncedTask( + mockProfile01.id, + status = SyncedTaskData.TaskStatus.Ready, + index = 0 + ), + MockPrescriptionInfo.MockSyncedPrescription.syncedTask( + mockProfile01.id, + status = SyncedTaskData.TaskStatus.InProgress, + index = 1 + ) + ) + ) + + val scannedTasks: MutableStateFlow> = + MutableStateFlow( + mutableListOf( + MockPrescriptionInfo.MockScannedPrescription.mockScannedTask01, + MockPrescriptionInfo.MockScannedPrescription.mockScannedTask02 + ) + ) + + val communications: MutableStateFlow> = + MutableStateFlow(mutableListOf()) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockConstants.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockConstants.kt new file mode 100644 index 00000000..36a7aba8 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockConstants.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.datasource.data + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.days + +object MockConstants { + internal val fixedTime = Instant.parse("2021-11-25T15:20:00Z") + internal val longerRandomTimeToday = Instant.parse("2021-11-25T15:20:00Z") + internal const val SYNCED_TASK_PRESET = "110.000.002.345.863" + internal val NOW = Clock.System.now() + internal val EXPIRY_DATE = Clock.System.now().plus(200.days) + internal val SHORT_EXPIRY_DATE = Clock.System.now().plus(20.days) + internal const val MOCK_COMMUNICATION_ID_01 = "CID-123-001" +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockPrescriptionInfo.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockPrescriptionInfo.kt new file mode 100644 index 00000000..8ba63628 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockPrescriptionInfo.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.datasource.data + +import de.gematik.ti.erp.app.datasource.data.MockConstants.EXPIRY_DATE +import de.gematik.ti.erp.app.datasource.data.MockConstants.NOW +import de.gematik.ti.erp.app.datasource.data.MockConstants.SHORT_EXPIRY_DATE +import de.gematik.ti.erp.app.datasource.data.MockConstants.SYNCED_TASK_PRESET +import de.gematik.ti.erp.app.datasource.data.MockConstants.fixedTime +import de.gematik.ti.erp.app.datasource.data.MockConstants.longerRandomTimeToday +import de.gematik.ti.erp.app.datasource.data.MockProfileInfo.mockProfile01 +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationDispense +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationPZN +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Quantity +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Ratio +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.utils.FhirTemporal +import java.util.UUID + +object MockPrescriptionInfo { + + private val BOOLEAN = listOf(true, false) + + private const val SYNCED_MEDICATION_NAMES = + "Ibuprofen 600" + + private const val SCANNED_MEDICINE_NAMES = "Lopressor" + + private const val DOSAGE = "1-0-1-1" + + private const val STREET_NAMES = "Marktplatz" + + private const val POSTAL_CODES = "10115" + + private const val CITY_NAMES = "Berlin" + + private const val FLOORS = "1. Stock" + + private const val PHONE_NUMBERS = "+49 123 4567890" + + private const val FIRST_NAMES = "Hans" + + private const val NAMES = "Hans Müller" + + private const val MEDICATION_SPECIALITIES = "Fachärztin für Innere Medizin" + + private val MEDICAL_PRACTICES = "Praxis Erika Mustermann" to "erika@mustermann.de" + + private const val DOCTORS_NOTES = "Patient hat grippeähnliche Symptome und sollte sich ausruhen." + + private const val normSizeMappings = "KA" + + private const val codeToFormMapping = "AEO" + + private const val PERFORMERS = "Apotheker" + + internal const val MOCK_IDENTIFIER = "1234567890" + + internal val PRACTITIONER = Practitioner( + name = NAMES, + qualification = MEDICATION_SPECIALITIES, + practitionerIdentifier = MOCK_IDENTIFIER + ) + + private val ADDRESS = SyncedTaskData.Address( + line1 = STREET_NAMES, + line2 = FLOORS, + postalCode = POSTAL_CODES, + city = CITY_NAMES + ) + + private fun organization(): Organization { + val item = MEDICAL_PRACTICES + return Organization( + name = item.first, + address = ADDRESS, + uniqueIdentifier = MOCK_IDENTIFIER, + phone = PHONE_NUMBERS, + mail = item.second + ) + } + + internal val ORGANIZATION = organization() + + internal val PATIENT = Patient( + name = "$FIRST_NAMES Mustermann", + address = ADDRESS, + birthdate = null, + insuranceIdentifier = MOCK_IDENTIFIER + ) + + private val RATIO = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ) + + private val MEDICATION = MedicationPZN( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = SYNCED_MEDICATION_NAMES, + form = codeToFormMapping, + lotNumber = MOCK_IDENTIFIER, + expirationDate = FhirTemporal.Instant(EXPIRY_DATE), + uniqueIdentifier = MOCK_IDENTIFIER, + normSizeCode = normSizeMappings, + amount = RATIO + ) + + internal val MEDICATION_DISPENSE = MedicationDispense( + dispenseId = UUID.randomUUID().toString(), + patientIdentifier = PATIENT.insuranceIdentifier ?: "", + medication = MEDICATION, + wasSubstituted = true, + dosageInstruction = DOSAGE, + performer = PERFORMERS, + whenHandedOver = null + ) + + internal var MEDICATION_REQUEST = MedicationRequest( + medication = MEDICATION, + dateOfAccident = null, + location = CITY_NAMES, + emergencyFee = true, + dosageInstruction = DOSAGE, + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = DOCTORS_NOTES, + substitutionAllowed = true + ) + + internal object MockScannedPrescription { + internal val mockScannedTask01 = ScannedTaskData.ScannedTask( + profileId = mockProfile01.id, + taskId = "160.000.006.394.157.15", + index = 0, + name = SCANNED_MEDICINE_NAMES, + accessCode = "8cc887c16681517e2db71078f367d4446c156bde743e15c2440722ec0835f406", + scannedOn = fixedTime, + redeemedOn = null, + communications = emptyList() + ) + internal val mockScannedTask02 = ScannedTaskData.ScannedTask( + profileId = mockProfile01.id, + taskId = "160.000.006.386.866.63", + index = 1, + name = SCANNED_MEDICINE_NAMES, + accessCode = "c0967e56ccbcb55ef0851ac9ad3a03dcfbb5ba1934d8d1338290167e348c876f", + scannedOn = fixedTime, + redeemedOn = null, + communications = emptyList() + ) + } + + internal object MockSyncedPrescription { + internal fun syncedTask( + profileIdentifier: ProfileIdentifier, + status: SyncedTaskData.TaskStatus = SyncedTaskData.TaskStatus.Ready, + index: Int + ) = SyncedTaskData.SyncedTask( + profileId = profileIdentifier, + taskId = "$SYNCED_TASK_PRESET.$index", + isIncomplete = false, + pvsIdentifier = MOCK_IDENTIFIER, + accessCode = MOCK_IDENTIFIER, + lastModified = longerRandomTimeToday, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = null, + status = null, + coverageType = SyncedTaskData.CoverageType.GKV + ), + expiresOn = EXPIRY_DATE, + acceptUntil = SHORT_EXPIRY_DATE, + authoredOn = NOW, + status = status, + medicationRequest = MEDICATION_REQUEST.copy( + substitutionAllowed = BOOLEAN[index] + ), // Making sure the substitutionAllowed is different for each task + medicationDispenses = listOf(MEDICATION_DISPENSE), + communications = emptyList(), + failureToReport = "" + ) + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockProfileInfo.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockProfileInfo.kt new file mode 100644 index 00000000..d1dec16a --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/datasource/data/MockProfileInfo.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.datasource.data + +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.datetime.Instant + +object MockProfileInfo { + private const val HEALTH_INSURANCE_COMPANIES = "GesundheitsVersichert AG" + + private const val INSURANCE_NUMBER = "10000000" + + internal val mockProfile01 = profile( + profileName = "Erika Mustermann", + isActive = true, + color = ProfilesData.ProfileColorNames.SUN_DEW, + insuranceType = ProfilesData.InsuranceType.PKV, // Note: Private insurance account + avatar = ProfilesData.Avatar.FemaleDoctor, + lastAuthenticated = null + ) + + private fun profile( + profileName: String, + isActive: Boolean = true, + color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.SUN_DEW, + avatar: ProfilesData.Avatar = ProfilesData.Avatar.FemaleDoctor, + insuranceType: ProfilesData.InsuranceType = ProfilesData.InsuranceType.GKV, + lastAuthenticated: Instant? = null + ): ProfilesData.Profile { + return ProfilesData.Profile( + id = "1", + name = profileName, + color = color, + avatar = avatar, + insuranceIdentifier = INSURANCE_NUMBER, + insuranceType = insuranceType, + insurantName = profileName, + insuranceName = HEALTH_INSURANCE_COMPANIES, + singleSignOnTokenScope = null, + active = isActive, + isConsentDrawerShown = false, + lastAuthenticated = lastAuthenticated + ) + } + + internal fun String.create() = profile(profileName = this) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt index 24cd9303..e0e98e0b 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt @@ -1,24 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di import android.content.Context +import android.content.res.AssetManager +import android.content.res.Resources +import android.os.Looper import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import de.gematik.ti.erp.app.DispatchProvider @@ -26,6 +29,7 @@ import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager import de.gematik.ti.erp.app.info.mockBuildConfigurationModule import de.gematik.ti.erp.app.pkv.mockFileProviderAuthorityModule import org.kodein.di.DI +import org.kodein.di.bindProvider import org.kodein.di.bindSingleton import org.kodein.di.instance @@ -38,8 +42,20 @@ const val ApplicationPreferencesTag = "ApplicationPreferences" const val NetworkPreferencesTag = "NetworkPreferences" const val NetworkSecurePreferencesTag = "NetworkSecurePreferences" -val appModules = DI.Module("appModules") { +val appModules = DI.Module("appModules", allowSilentOverride = true) { bindSingleton { object : DispatchProvider {} } + bindProvider { + val context = instance() + context.resources + } + bindProvider { + val context = instance() + context.assets + } + bindProvider { + val context = instance() + context.mainLooper + } bindSingleton(ApplicationPreferencesTag) { val context = instance() context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt index 78a1ab3e..7153ccd9 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt @@ -1,43 +1,51 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di import de.gematik.ti.erp.app.analytics.di.analyticsModule import de.gematik.ti.erp.app.appsecurity.appSecurityModule +import de.gematik.ti.erp.app.appupdate.di.appUpdateModule import de.gematik.ti.erp.app.authentication.di.authenticationModule import de.gematik.ti.erp.app.cardunlock.di.cardUnlockModule import de.gematik.ti.erp.app.cardwall.cardWallModule +import de.gematik.ti.erp.app.debugsettings.di.debugSettingsModule +import de.gematik.ti.erp.app.di.datasource.mockDataSourceModule +import de.gematik.ti.erp.app.di.pharmacy.mockPharmacyRepositoryModule +import de.gematik.ti.erp.app.di.prescription.mockPrescriptionRepositoryModule +import de.gematik.ti.erp.app.di.prescription.mockTaskRepositoryModule +import de.gematik.ti.erp.app.di.profile.mockProfileRepositoryModule +import de.gematik.ti.erp.app.di.settings.mockSettingsRepositoryModule +import de.gematik.ti.erp.app.featuretoggle.di.newFeaturesSharedPrefsModule import de.gematik.ti.erp.app.idp.idpModule import de.gematik.ti.erp.app.idp.idpUseCaseModule +import de.gematik.ti.erp.app.logger.di.loggerModule +import de.gematik.ti.erp.app.messages.di.messageRepositoryModule +import de.gematik.ti.erp.app.messages.di.messagesModule import de.gematik.ti.erp.app.mlkit.mlKitModule -import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule -import de.gematik.ti.erp.app.orders.messageRepositoryModule -import de.gematik.ti.erp.app.orders.messagesModule -import de.gematik.ti.erp.app.pharmacy.di.pharmacyRepositoryModule -import de.gematik.ti.erp.app.pharmacy.pharmacyMockModule +import de.gematik.ti.erp.app.onboarding.di.onboardingModule +import de.gematik.ti.erp.app.orderhealthcard.di.orderHealthCardModule +import de.gematik.ti.erp.app.pharmacy.di.pharmacyModule import de.gematik.ti.erp.app.pkv.consentRepositoryModule import de.gematik.ti.erp.app.pkv.pkvModule import de.gematik.ti.erp.app.prescription.prescriptionModule import de.gematik.ti.erp.app.prescription.prescriptionRepositoryModule import de.gematik.ti.erp.app.prescription.taskModule -import de.gematik.ti.erp.app.prescription.taskRepositoryModule -import de.gematik.ti.erp.app.profiles.profileRepositoryModule import de.gematik.ti.erp.app.profiles.profilesModule import de.gematik.ti.erp.app.protocol.protocolModule import de.gematik.ti.erp.app.protocol.protocolRepositoryModule @@ -49,16 +57,20 @@ import org.kodein.di.DI val mockFeatureModule = DI.Module("featureModule", allowSilentOverride = true) { importAll( + applicationControllerModule, + onboardingModule, + dispatchersModule, cardWallModule, appSecurityModule, networkModule, + loggerModule, realmModule, idpModule, idpUseCaseModule, messagesModule, orderHealthCardModule, + pharmacyModule, redeemModule, - prescriptionModule, profilesModule, protocolModule, taskModule, @@ -67,18 +79,27 @@ val mockFeatureModule = DI.Module("featureModule", allowSilentOverride = true) { cardUnlockModule, pkvModule, authenticationModule, - profileRepositoryModule, + // shared-prefs modules + timeoutsSharedPrefsModule, + newFeaturesSharedPrefsModule, + // other modules + analyticsModule, + debugSettingsModule, + mlKitModule, + appUpdateModule, + prescriptionModule, + // repositories prescriptionRepositoryModule, consentRepositoryModule, protocolRepositoryModule, - pharmacyRepositoryModule, messageRepositoryModule, - taskRepositoryModule, - timeoutsSharedPrefsModule, - analyticsModule, - mlKitModule, // mocked modules - pharmacyMockModule, + mockPharmacyRepositoryModule, + mockTaskRepositoryModule, + mockProfileRepositoryModule, + mockDataSourceModule, + mockPrescriptionRepositoryModule, + mockSettingsRepositoryModule, allowOverride = true ) } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/datasource/MockDataSourceModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/datasource/MockDataSourceModule.kt new file mode 100644 index 00000000..cf096319 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/datasource/MockDataSourceModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.datasource + +import de.gematik.ti.erp.app.datasource.MockDataSource +import org.kodein.di.DI +import org.kodein.di.bindSingleton + +val mockDataSourceModule = DI.Module("mockDataSourceModule") { + bindSingleton { MockDataSource() } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/overrides/TestConfigOverrides.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/overrides/TestConfigOverrides.kt new file mode 100644 index 00000000..b4aa6a03 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/overrides/TestConfigOverrides.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.overrides + +import de.gematik.ti.erp.app.config.TestScenario +import de.gematik.ti.erp.app.mocks.settings.OnboardingDoneMockSettingsDataSource +import de.gematik.ti.erp.app.mocks.settings.OnboardingNotDoneMockSettingsDataSource +import de.gematik.ti.erp.app.settings.datasource.SettingsDataSource +import org.kodein.di.DI +import org.kodein.di.bindProvider + +/** + * Overrides designed specifically for different test scenarios + */ +fun DI.MainBuilder.testScenarioOverrides(scenario: TestScenario) { + when (scenario.isOnboardingDone) { + true -> bindProvider(overrides = true) { OnboardingDoneMockSettingsDataSource() } + false -> bindProvider(overrides = true) { OnboardingNotDoneMockSettingsDataSource() } + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/pharmacy/MockPharmacyModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/pharmacy/MockPharmacyModule.kt new file mode 100644 index 00000000..3f13ec45 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/pharmacy/MockPharmacyModule.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.pharmacy + +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.PreviewMapCoordinatesRepository +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.repository.datasource.DefaultFavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.DefaultOftenUsePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PharmacyRemoteDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PreviewMapCoordinatesDataSource +import de.gematik.ti.erp.app.redeem.repository.datasource.DefaultRedeemLocalDataSource +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource +import de.gematik.ti.erp.app.repository.MockShippingContactRepository +import de.gematik.ti.erp.app.repository.pharmacy.MockPharmacyRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val mockPharmacyRepositoryModule = DI.Module("mockPharmacyModule") { + bindProvider { PharmacyRemoteDataSource(instance(), instance()) } + bindProvider { DefaultRedeemLocalDataSource(instance()) } + bindProvider { DefaultOftenUsePharmacyLocalDataSource(instance()) } + bindProvider { DefaultFavouritePharmacyLocalDataSource(instance()) } + bindSingleton { PreviewMapCoordinatesDataSource() } + bindProvider { PreviewMapCoordinatesRepository(instance()) } + bindProvider { MockPharmacyRepository(instance(), instance(), instance()) } + bindProvider { MockShippingContactRepository() } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockPrescriptionModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockPrescriptionModule.kt new file mode 100644 index 00000000..6aa7910d --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockPrescriptionModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.prescription + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.repository.prescription.MockPrescriptionsRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val mockPrescriptionRepositoryModule = DI.Module("mockPrescriptionRepositoryModule", allowSilentOverride = true) { + bindProvider { MockPrescriptionsRepository(instance()) } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockTaskModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockTaskModule.kt new file mode 100644 index 00000000..cfa3ba70 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/prescription/MockTaskModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.prescription + +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.repository.prescription.MockTaskRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val mockTaskRepositoryModule = DI.Module("mockTaskRepositoryModule", allowSilentOverride = true) { + bindProvider { MockTaskRepository(instance()) } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/profile/MockProfileModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/profile/MockProfileModule.kt new file mode 100644 index 00000000..0b7a1ce5 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/profile/MockProfileModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.profile + +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.repository.profiles.MockProfilesRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val mockProfileRepositoryModule = DI.Module("mockProfileRepositoryModule", allowSilentOverride = true) { + bindProvider { + MockProfilesRepository(instance()) + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/settings/SettingsMockModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/settings/SettingsMockModule.kt new file mode 100644 index 00000000..1aa2cf09 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/settings/SettingsMockModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di.settings + +import de.gematik.ti.erp.app.mocks.settings.OnboardingDoneMockSettingsDataSource +import de.gematik.ti.erp.app.repository.MockSettingsRepository +import de.gematik.ti.erp.app.settings.ApplicationPreferencesTag +import de.gematik.ti.erp.app.settings.datasource.SettingsDataSource +import de.gematik.ti.erp.app.settings.repository.CardWallRepository +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val mockSettingsRepositoryModule = DI.Module("mockSettingsModule", allowSilentOverride = true) { + bindProvider { CardWallRepository(prefs = instance(ApplicationPreferencesTag)) } + bindProvider { MockSettingsRepository(instance(), instance(), instance()) } + bindSingleton { OnboardingDoneMockSettingsDataSource() } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt index 0cdbffb5..0b902271 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.info @@ -39,4 +39,5 @@ class MockBuildConfigInformation : BuildConfigInformation { @Composable override fun inDarkTheme(): String = if (isSystemInDarkTheme()) DARK_THEME_ON else DARK_THEME_OFF override fun nfcInformation(context: Context): String = "nicht vorhanden" + override fun isMockedApp(): Boolean = true } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt index 5eeee5bc..2db2837d 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.info diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/profile/ProfileDataSource.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/profile/ProfileDataSource.kt new file mode 100644 index 00000000..6ca2e95b --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/profile/ProfileDataSource.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.profile + +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.datetime.Clock +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.util.encoders.Base64 +import java.util.UUID +import kotlin.time.Duration.Companion.days + +class ProfileDataSource { + + private val can = "123123" + private val byteArray = Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) + private val healthCertificate = X509CertificateHolder(byteArray) + private val singleSignOnToken = IdpData.SingleSignOnToken( + token = UUID.randomUUID().toString(), + expiresOn = Clock.System.now().plus(200.days), + validOn = Clock.System.now().plus(20.days) + ) + + private val firstProfile = ProfilesData.Profile( + id = "1", + name = "Max Mustermann", + color = ProfilesData.ProfileColorNames.BLUE_MOON, + lastAuthenticated = Clock.System.now(), + avatar = ProfilesData.Avatar.ManWithPhone, + insuranceName = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + active = true, + singleSignOnTokenScope = IdpData.DefaultToken( + token = singleSignOnToken, + cardAccessNumber = can, + healthCardCertificate = healthCertificate + ) + ) + + val profiles = MutableStateFlow(mutableListOf(firstProfile)) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingDoneMockSettingsDataSource.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingDoneMockSettingsDataSource.kt new file mode 100644 index 00000000..08ec6cc5 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingDoneMockSettingsDataSource.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.settings + +import de.gematik.ti.erp.app.settings.datasource.SettingsDataSource +import de.gematik.ti.erp.app.settings.model.SettingsData +import kotlinx.coroutines.flow.MutableStateFlow + +class OnboardingDoneMockSettingsDataSource : SettingsDataSource { + override val appVersion = SETTINGS_APP_VERSION_DATA + + override val authentication: MutableStateFlow = + MutableStateFlow(SETTINGS_PASSWORD) + + override val pharmacySearch: MutableStateFlow = + MutableStateFlow(SETTINGS_PHARMACY_SEARCH_RESULT_DATA) + + override val generalData: MutableStateFlow = + MutableStateFlow( + SETTINGS_GENERAL_DATA.copy(onboardingShownIn = appVersion) + ) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingNotDoneMockSettingsDataSource.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingNotDoneMockSettingsDataSource.kt new file mode 100644 index 00000000..1213b3d8 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/OnboardingNotDoneMockSettingsDataSource.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.settings + +import de.gematik.ti.erp.app.settings.datasource.SettingsDataSource +import de.gematik.ti.erp.app.settings.model.SettingsData +import kotlinx.coroutines.flow.MutableStateFlow + +class OnboardingNotDoneMockSettingsDataSource : SettingsDataSource { + override val appVersion = SETTINGS_APP_VERSION_DATA + + override val authentication: MutableStateFlow = + MutableStateFlow(SETTINGS_UNSPECIFIED) + + override val pharmacySearch: MutableStateFlow = + MutableStateFlow(SETTINGS_PHARMACY_SEARCH_RESULT_DATA) + + override val generalData: MutableStateFlow = + MutableStateFlow(SETTINGS_GENERAL_DATA) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/SettingsMocks.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/SettingsMocks.kt new file mode 100644 index 00000000..e9094cc8 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/mocks/settings/SettingsMocks.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.settings + +import de.gematik.ti.erp.app.settings.model.SettingsData + +internal val SETTINGS_APP_VERSION_DATA = SettingsData.AppVersion( + code = 1, + name = "1.0.0" +) + +internal val SETTINGS_PHARMACY_SEARCH_RESULT_DATA = SettingsData.PharmacySearch( + name = "TEST-ONLY", + locationEnabled = true, + deliveryService = false, + onlineService = false, + openNow = false +) + +internal val SETTINGS_GENERAL_DATA = SettingsData.General( + latestAppVersion = SETTINGS_APP_VERSION_DATA, + onboardingShownIn = null, // app will show onboarding if this is null + welcomeDrawerShown = true, + zoomEnabled = false, + userHasAcceptedInsecureDevice = true, + mainScreenTooltipsShown = true, + mlKitAccepted = false, + screenShotsAllowed = true, + trackingAllowed = false, + userHasAcceptedIntegrityNotOk = true +) + +internal val SETTINGS_PASSWORD = SettingsData.Authentication( + password = SettingsData.Authentication.Password(password = "password"), + deviceSecurity = false, + failedAuthenticationAttempts = 0 +) + +internal val SETTINGS_UNSPECIFIED = SettingsData.Authentication( + password = null, + deviceSecurity = false, + failedAuthenticationAttempts = 0 +) diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockProfileLinkedCommunication.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockProfileLinkedCommunication.kt new file mode 100644 index 00000000..28eac055 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockProfileLinkedCommunication.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.model + +import de.gematik.ti.erp.app.datasource.data.MockConstants.MOCK_COMMUNICATION_ID_01 +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * This is created to link the communication with a particular profile + */ +data class MockProfileLinkedCommunication( + val profileId: String, + val taskId: String, + val communicationId: String, + val orderId: String, + val profile: CommunicationProfile, + val sentOn: Instant, + val sender: String, + val recipient: String, + val payload: String?, + val consumed: Boolean +) + +fun MockProfileLinkedCommunication.toSyncedTaskDataCommunication() = + Communication( + taskId = taskId, + communicationId = communicationId, + orderId = orderId, + profile = profile, + sentOn = sentOn, + sender = sender, + recipient = recipient, + payload = payload, + consumed = consumed + ) + +internal fun MockSentCommunicationJson.toMockProfileLinkedCommunication( + profileId: ProfileIdentifier +) = + MockProfileLinkedCommunication( + profileId = profileId, + communicationId = MOCK_COMMUNICATION_ID_01, + taskId = basedOn.firstNotNullOfOrNull { it.taskId } ?: "", + orderId = identifier.firstNotNullOfOrNull { it.value } ?: "", + profile = when (meta.isRequest) { + true -> CommunicationProfile.ErxCommunicationDispReq + false -> CommunicationProfile.ErxCommunicationReply + }, + sentOn = Clock.System.now(), + sender = payload.firstNotNullOfOrNull { it.name } ?: "", + recipient = recipient.firstNotNullOfOrNull { it.identifier.value } ?: "", + payload = payload.firstNotNullOfOrNull { it.contentString }, + consumed = false + ) diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockSentCommunicationJson.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockSentCommunicationJson.kt new file mode 100644 index 00000000..a9c4510c --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/model/MockSentCommunicationJson.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.model + +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.serialization.Serializable +import org.json.JSONObject + +@Serializable +data class MockSentCommunicationJson( + val resourceType: String, + val meta: MockSentCommunicationMeta, + val identifier: List, + val status: String, + val basedOn: List, + val recipient: List, + val payload: List +) + +@Serializable +data class MockSentCommunicationMeta( + val profile: List +) { + val isRequest = profile.any { it.contains("GEM_ERP_PR_Communication_DispReq") } +} + +@Serializable +data class MockCommunicationOrderIdIdentifier( + val system: String, + // order-id + val value: String +) + +@Serializable +data class MockCommunicationTaskIdIdentifierReference( + val reference: String +) { + val taskId = reference.split('/').getOrNull(1) // task-id +} + +@Serializable +data class MockCommunicationRecipient( + val identifier: MockCommunicationRecipientBundle +) + +@Serializable +data class MockCommunicationRecipientBundle( + val system: String, + val value: String // telematik.id +) + +@Serializable +data class MockCommunicationPayloadContent( + val contentString: String +) { + private fun jsonObject() = JSONObject(contentString) + val name: String + get() = jsonObject().getString("name") + + val supplyOptionType: String + get() = jsonObject().getString("supplyOptionsType") + + val address: SyncedTaskData.Address + get() { + val item = jsonObject().getString("address").split(',') + return SyncedTaskData.Address( + line1 = item[0], + line2 = item[1], + postalCode = item[2], + city = item[3] + ) + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt deleted file mode 100644 index b6024e1a..00000000 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy - -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRemoteDataSource -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository -import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository -import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.repository.PharmacyMockRepository -import org.kodein.di.DI -import org.kodein.di.bindProvider -import org.kodein.di.instance - -val pharmacyMockModule = DI.Module("pharmacyMockModule") { - bindProvider { PharmacyRemoteDataSource(instance(), instance()) } - bindProvider { PharmacyMockRepository() } - bindProvider { ShippingContactRepository(instance(), instance()) } - bindProvider { PharmacyDirectRedeemUseCase(instance()) } - bindProvider { PharmacyMapsUseCase(instance(), instance(), instance()) } - bindProvider { PharmacySearchUseCase(instance(), instance(), instance(), instance(), instance()) } - bindProvider { PharmacyOverviewUseCase(instance(), instance()) } - bindProvider { GetOrderStateUseCase(instance(), instance(), instance()) } - bindProvider { GetOverviewPharmaciesUseCase(instance()) } -} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt index 0707084a..12cc48e8 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt index fbf165f1..0bbe5c71 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.pkv import org.kodein.di.DI diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockSettingsRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockSettingsRepository.kt new file mode 100644 index 00000000..bfccc322 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockSettingsRepository.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.repository + +import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 +import de.gematik.ti.erp.app.db.writeToRealm +import de.gematik.ti.erp.app.settings.datasource.SettingsDataSource +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import io.realm.kotlin.Realm +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +class MockSettingsRepository( + private val settingsDataSource: SettingsDataSource, + // keep realm till the profile mock is implemented + private val realm: Realm, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : SettingsRepository( + dispatchers = dispatcher, + realm = realm +) { + override val general: Flow + get() = settingsDataSource.generalData + + override suspend fun acceptUpdatedDataTerms(now: Instant) { + // no-op + } + + override suspend fun saveOnboardingData( + authentication: SettingsData.Authentication, + profileName: String, + now: Instant + ) { + withContext(dispatcher) { + if (authentication.methodIsPassword) { + settingsDataSource.authentication.value = authentication + } + } + withContext(dispatcher) { + realm.writeToRealm { profile -> + copyToRealm( + ProfileEntityV1().apply { + this.name = profileName + this.active = true + } + ) + } + } + } + + override suspend fun enableDeviceSecurity() { + // no-op since we only support password authentication for mock + } + + override suspend fun disableDeviceSecurity() { + // no-op since we only support password authentication for mock + } + + override suspend fun setPassword(password: SettingsData.Authentication.Password) { + settingsDataSource.authentication.value = SettingsData.Authentication( + password = password, + deviceSecurity = false, + failedAuthenticationAttempts = 0 + ) + } + + override suspend fun resetPassword() { + // no-op since we only support password authentication for mock + } + + override val authentication: Flow + get() = settingsDataSource.authentication + + override suspend fun saveZoomPreference(enabled: Boolean) { + settingsDataSource.generalData.update { + it.copy( + zoomEnabled = enabled + ) + } + } + + override suspend fun acceptInsecureDevice() { + settingsDataSource.generalData.update { + it.copy( + userHasAcceptedInsecureDevice = true + ) + } + } + + override suspend fun incrementNumberOfAuthenticationFailures() { + settingsDataSource.authentication.update { + it.copy( + failedAuthenticationAttempts = it.failedAuthenticationAttempts + 1 + ) + } + } + + override suspend fun resetNumberOfAuthenticationFailures() { + settingsDataSource.authentication.update { + it.copy( + failedAuthenticationAttempts = 0 + ) + } + } + + override suspend fun saveWelcomeDrawerShown() { + settingsDataSource.generalData.update { + it.copy( + welcomeDrawerShown = true + ) + } + } + + override suspend fun saveMainScreenTooltipShown() { + settingsDataSource.generalData.update { + it.copy( + mainScreenTooltipsShown = true + ) + } + } + + override suspend fun acceptMlKit() { + settingsDataSource.generalData.update { + it.copy( + mlKitAccepted = true + ) + } + } + + override suspend fun saveAllowScreenshots(allow: Boolean) { + settingsDataSource.generalData.update { + it.copy( + screenShotsAllowed = allow + ) + } + } + + override suspend fun saveAllowTracking(allow: Boolean) { + settingsDataSource.generalData.update { + it.copy( + trackingAllowed = allow + ) + } + } + + override suspend fun acceptIntegrityNotOk() { + settingsDataSource.generalData.update { + it.copy( + userHasAcceptedIntegrityNotOk = true + ) + } + } + + override suspend fun savePharmacySearch(search: SettingsData.PharmacySearch) { + settingsDataSource.pharmacySearch.update { + it.copy( + name = search.name, + locationEnabled = search.locationEnabled, + deliveryService = search.deliveryService, + onlineService = search.onlineService, + openNow = search.openNow + ) + } + } + + override val pharmacySearch: Flow + get() = settingsDataSource.pharmacySearch + + override fun isAnalyticsAllowed(): Flow { + return settingsDataSource.generalData.map { it.trackingAllowed } + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockShippingContactRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockShippingContactRepository.kt new file mode 100644 index 00000000..bdaa3a83 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/MockShippingContactRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.repository + +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class MockShippingContactRepository : ShippingContactRepository { + override fun shippingContact(): Flow { + return flowOf( + PharmacyData.ShippingContact( + name = "Helga Schmetterling", + line1 = "Schmetterlingweg 1", + line2 = "2 Stockwerk rechts", + postalCode = "12345", + city = "Berlin", + telephoneNumber = "123456789", + mail = "schmetterling@butterfly.com", + deliveryInformation = "Bitte klingeln" + ) + ) + } + + override suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) { + // Add implementation + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt deleted file mode 100644 index 0788e7b0..00000000 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt +++ /dev/null @@ -1,1993 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.repository - -import de.gematik.ti.erp.app.fhir.model.PharmacyServices -import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices -import de.gematik.ti.erp.app.fhir.model.json -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -@Suppress("LargeClass") -class PharmacyMockRepository : PharmacyRepository { - - override suspend fun searchPharmacies(names: List, filter: Map): Result { - return Result.success(extractedPharmacies) - } - - override suspend fun searchPharmaciesByBundle(bundleId: String, offset: Int, count: Int): Result { - return Result.success(extractedPharmacies) - } - - override suspend fun searchBinaryCerts(locationId: String): Result> { - return Result.success(emptyList()) - } - - override suspend fun redeemPrescriptionDirectly( - url: String, - message: ByteArray, - pharmacyTelematikId: String, - transactionId: String - ): Result { - return Result.success(Unit) - } - - override fun loadOftenUsedPharmacies(): Flow> { - return flowOf(emptyList()) - } - - override fun loadFavoritePharmacies(): Flow> { - return flowOf(emptyList()) - } - - @Suppress("EmptyFunctionBlock") - override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - } - - @Suppress("EmptyFunctionBlock") - override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - } - - @Suppress("EmptyFunctionBlock") - override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - } - - @Suppress("EmptyFunctionBlock") - override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { - } - - override suspend fun searchPharmacyByTelematikId(telematikId: String): Result { - return Result.success(extractedPharmacies) - } - - override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow { - return flowOf(true) - } - - @Suppress("EmptyFunctionBlock") - override suspend fun markAsRedeemed(taskId: String) { - } - - private val jsonStringMocked = """{ - "id": "49b6b9fd-eec7-41f3-b624-cc99d46fb828", - "meta": { - "lastUpdated": "2023-08-31T12:18:10.94674676+02:00" - }, - "resourceType": "Bundle", - "type": "searchset", - "total": 20, - "link": [ - { - "relation": "self", - "url": "Bundle49b6b9fd-eec7-41f3-b624-cc99d46fb828" - } - ], - "entry": [ - { - "resource": { - "id": "6bb01538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 01" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.201" - } - ], - "name": "ZoTI_01_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb02538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 02" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.202" - } - ], - "name": "ZoTI_02_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb03538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 03" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.203" - } - ], - "name": "ZoTI_03_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb04538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 04" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.204" - } - ], - "name": "ZoTI_04_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 300 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb05538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 05" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.205" - } - ], - "name": "ZoTI_05_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb06538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "2d1f1f35-d03d-4932-a78a-67715cbb7963", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb06538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 06" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.206" - } - ], - "name": "ZoTI_06_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb07538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 07" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.207" - } - ], - "name": "ZoTI_07_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb08538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 08" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.208" - } - ], - "name": "ZoTI_08_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb09538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 09" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.209" - } - ], - "name": "ZoTI_09_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb10538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "fe9a01e8-d702-4b9d-a997-096eca057b74", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb10538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 10" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.210" - } - ], - "name": "ZoTI_10_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb11538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "0991992b-b3fd-4f3e-a331-d4f0e2856185", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb11538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 11" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.211" - } - ], - "name": "ZoTI_11_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb12538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "ae48f60e-9c17-4610-a0c4-d1f7ac6abb5b", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb12538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 12" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.212" - } - ], - "name": "ZoTI_12_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb13538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "e63f85da-3c1a-4f16-8059-45321bec107f", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb13538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 13" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.213" - } - ], - "name": "ZoTI_13_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb14538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "bb022669-f8fc-424a-8bfa-e9b5e8102333", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb14538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 14" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.214" - } - ], - "name": "ZoTI_14_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb15538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 15" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.215" - } - ], - "name": "ZoTI_15_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb16538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 16" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.216" - } - ], - "name": "ZoTI_16_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb17538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "0f4ae22e-f717-47b1-893e-1d41684c8579", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb17538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 17" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.217" - } - ], - "name": "ZoTI_17_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb18538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 18" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.218" - } - ], - "name": "ZoTI_18_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", - "use": "mobile", - "rank": 100 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb19538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "2" - }, - "resourceType": "Location", - "contained": [ - { - "id": "72ab1d02-d3e2-4af1-891f-a476c23eaf44", - "resourceType": "HealthcareService", - "active": true, - "coverageArea": [ - { - "extension": [ - { - "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", - "valueQuantity": { - "value": 10000, - "unit": "m" - } - } - ] - } - ], - "location": [ - { - "reference": "/Location/6bb1 2023-08-31 12:18:11.626 28938-31101 OkHttp de.gematik.ti.erp.app.test D 9538-5924-4be3-98ff-7475d27aee4f" - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "498", - "display": "Mobile Services" - } - ] - } - ] - } - ], - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 19" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.219" - } - ], - "name": "ZoTI_19_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "telecom": [ - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", - "use": "mobile", - "rank": 300 - }, - { - "system": "other", - "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", - "use": "mobile", - "rank": 200 - } - ], - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "PHARM", - "display": "pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - }, - { - "resource": { - "id": "6bb20538-5924-4be3-98ff-7475d27aee4f", - "meta": { - "lastUpdated": "2023-08-07T10:48:51.845+02:00", - "versionId": "3" - }, - "resourceType": "Location", - "address": { - "type": "physical", - "line": [ - "ZoTIstr. 20" - ], - "postalCode": "10117", - "city": "ZoTI-Town", - "country": "D" - }, - "hoursOfOperation": [ - { - "daysOfWeek": [ - "mon", - "tue", - "wed", - "thu", - "fri" - ], - "openingTime": "08:00:00", - "closingTime": "18:00:00" - } - ], - "identifier": [ - { - "system": "https://gematik.de/fhir/NamingSystem/TelematikID", - "value": "3-01.2.2023001.16.220" - } - ], - "name": "ZoTI_20_TEST-ONLY", - "position": { - "latitude": 13.387627883956709, - "longitude": 52.5226398750957 - }, - "status": "active", - "type": [ - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/service-type", - "code": "DELEGATOR", - "display": "eRX Token Receiver" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "OUTPHARM", - "display": "outpatient pharmacy" - } - ] - }, - { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", - "code": "MOBL", - "display": "Mobile Services" - } - ] - } - ] - }, - "search": { - "mode": "match" - } - } - ] -}""" - private val jsonMockedData = json.parseToJsonElement(jsonStringMocked) - private val extractedPharmacies = - extractPharmacyServices( - bundle = jsonMockedData, - onError = { jsonElement, cause -> - Napier.e(cause) { - jsonElement.toString() - } - } - ) -} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/pharmacy/MockPharmacyRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/pharmacy/MockPharmacyRepository.kt new file mode 100644 index 00000000..214eeb02 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/pharmacy/MockPharmacyRepository.kt @@ -0,0 +1,1988 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.repository.pharmacy + +import de.gematik.ti.erp.app.fhir.model.PharmacyServices +import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices +import de.gematik.ti.erp.app.fhir.model.json +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +@Suppress("LargeClass") +class MockPharmacyRepository( + private val favouriteLocalDataSource: FavouritePharmacyLocalDataSource, + private val oftenUsedLocalDataSource: OftenUsedPharmacyLocalDataSource, + private val redeemLocalDataSource: RedeemLocalDataSource +) : PharmacyRepository { + + override suspend fun searchPharmacies(names: List, filter: Map): Result { + return Result.success(extractedPharmacies) + } + + override suspend fun searchPharmaciesByBundle(bundleId: String, offset: Int, count: Int): Result { + return Result.success(extractedPharmacies) + } + + override suspend fun searchBinaryCerts(locationId: String): Result> { + return Result.success(emptyList()) + } + + override suspend fun redeemPrescriptionDirectly( + url: String, + message: ByteArray, + pharmacyTelematikId: String, + transactionId: String + ): Result { + return Result.success(Unit) + } + + override fun loadOftenUsedPharmacies(): Flow> = oftenUsedLocalDataSource.loadOftenUsedPharmacies() + + override fun loadFavoritePharmacies(): Flow> = favouriteLocalDataSource.loadFavoritePharmacies() + + override suspend fun markPharmacyAsOftenUsed(pharmacy: PharmacyUseCaseData.Pharmacy) = oftenUsedLocalDataSource.markPharmacyAsOftenUsed(pharmacy) + + override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) = + oftenUsedLocalDataSource.deleteOverviewPharmacy(overviewPharmacy) + + override suspend fun markPharmacyAsFavourite(pharmacy: PharmacyUseCaseData.Pharmacy) = favouriteLocalDataSource.markPharmacyAsFavourite(pharmacy) + + override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) = + favouriteLocalDataSource.deleteFavoritePharmacy(favoritePharmacy) + + override suspend fun searchPharmacyByTelematikId(telematikId: String): Result { + return Result.success(extractedPharmacies) + } + + override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow { + return flowOf(true) + } + + override suspend fun markAsRedeemed(taskId: String) = redeemLocalDataSource.markAsRedeemed(taskId) + + private val jsonStringMocked = """{ + "id": "49b6b9fd-eec7-41f3-b624-cc99d46fb828", + "meta": { + "lastUpdated": "2023-08-31T12:18:10.94674676+02:00" + }, + "resourceType": "Bundle", + "type": "searchset", + "total": 20, + "link": [ + { + "relation": "self", + "url": "Bundle49b6b9fd-eec7-41f3-b624-cc99d46fb828" + } + ], + "entry": [ + { + "resource": { + "id": "6bb01538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 01" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.201" + } + ], + "name": "ZoTI_01_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb02538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 02" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.202" + } + ], + "name": "ZoTI_02_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb03538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 03" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.203" + } + ], + "name": "ZoTI_03_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb04538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 04" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.204" + } + ], + "name": "ZoTI_04_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 300 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb05538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 05" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.205" + } + ], + "name": "ZoTI_05_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb06538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "2d1f1f35-d03d-4932-a78a-67715cbb7963", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb06538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 06" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.206" + } + ], + "name": "ZoTI_06_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb07538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 07" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.207" + } + ], + "name": "ZoTI_07_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb08538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 08" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.208" + } + ], + "name": "ZoTI_08_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb09538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 09" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.209" + } + ], + "name": "ZoTI_09_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb10538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "fe9a01e8-d702-4b9d-a997-096eca057b74", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb10538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 10" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.210" + } + ], + "name": "ZoTI_10_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb11538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "0991992b-b3fd-4f3e-a331-d4f0e2856185", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb11538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 11" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.211" + } + ], + "name": "ZoTI_11_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb12538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "ae48f60e-9c17-4610-a0c4-d1f7ac6abb5b", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb12538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 12" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.212" + } + ], + "name": "ZoTI_12_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb13538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "e63f85da-3c1a-4f16-8059-45321bec107f", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb13538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 13" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.213" + } + ], + "name": "ZoTI_13_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb14538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "bb022669-f8fc-424a-8bfa-e9b5e8102333", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb14538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 14" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.214" + } + ], + "name": "ZoTI_14_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb15538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 15" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.215" + } + ], + "name": "ZoTI_15_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb16538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 16" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.216" + } + ], + "name": "ZoTI_16_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb17538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "0f4ae22e-f717-47b1-893e-1d41684c8579", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb17538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 17" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.217" + } + ], + "name": "ZoTI_17_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb18538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 18" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.218" + } + ], + "name": "ZoTI_18_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use": "mobile", + "rank": 100 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb19538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "2" + }, + "resourceType": "Location", + "contained": [ + { + "id": "72ab1d02-d3e2-4af1-891f-a476c23eaf44", + "resourceType": "HealthcareService", + "active": true, + "coverageArea": [ + { + "extension": [ + { + "url": "https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity": { + "value": 10000, + "unit": "m" + } + } + ] + } + ], + "location": [ + { + "reference": "/Location/6bb1 2023-08-31 12:18:11.626 28938-31101 OkHttp de.gematik.ti.erp.app.test D 9538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "498", + "display": "Mobile Services" + } + ] + } + ] + } + ], + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 19" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.219" + } + ], + "name": "ZoTI_19_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use": "mobile", + "rank": 300 + }, + { + "system": "other", + "value": "https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use": "mobile", + "rank": 200 + } + ], + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "PHARM", + "display": "pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + }, + { + "resource": { + "id": "6bb20538-5924-4be3-98ff-7475d27aee4f", + "meta": { + "lastUpdated": "2023-08-07T10:48:51.845+02:00", + "versionId": "3" + }, + "resourceType": "Location", + "address": { + "type": "physical", + "line": [ + "ZoTIstr. 20" + ], + "postalCode": "10117", + "city": "ZoTI-Town", + "country": "D" + }, + "hoursOfOperation": [ + { + "daysOfWeek": [ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime": "08:00:00", + "closingTime": "18:00:00" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-01.2.2023001.16.220" + } + ], + "name": "ZoTI_20_TEST-ONLY", + "position": { + "latitude": 13.387627883956709, + "longitude": 52.5226398750957 + }, + "status": "active", + "type": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/service-type", + "code": "DELEGATOR", + "display": "eRX Token Receiver" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "OUTPHARM", + "display": "outpatient pharmacy" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + } + ] +}""" + private val jsonMockedData = json.parseToJsonElement(jsonStringMocked) + private val extractedPharmacies = + extractPharmacyServices( + bundle = jsonMockedData, + onError = { jsonElement, cause -> + Napier.e(cause) { + jsonElement.toString() + } + } + ) +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockPrescriptionsRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockPrescriptionsRepository.kt new file mode 100644 index 00000000..45188f87 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockPrescriptionsRepository.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.repository.prescription + +import de.gematik.ti.erp.app.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.datasource.MockDataSource +import de.gematik.ti.erp.app.model.MockSentCommunicationJson +import de.gematik.ti.erp.app.model.toMockProfileLinkedCommunication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData.ScannedTask +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.SyncedTask +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement + +class MockPrescriptionsRepository( + private val dataSource: MockDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : PrescriptionRepository { + override suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List, + medicationString: String + ) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + scannedList.addAll(tasks) + scannedList + } + } + } + + override fun scannedTasks(profileId: ProfileIdentifier): Flow> = dataSource.scannedTasks + .map { list -> list.filter { it.profileId == profileId } } + + override fun syncedTasks(profileId: ProfileIdentifier): Flow> = + dataSource.syncedTasks.mapNotNull { taskList -> + taskList.filter { it.profileId == profileId }.sortedBy { it.lastModified } + }.flowOn(dispatcher) + + override suspend fun redeemPrescription( + profileId: ProfileIdentifier, + communication: JsonElement, + accessCode: String + ): Result = + withContext(dispatcher) { + val decodedCommunication = Json + .decodeFromJsonElement(communication) + .toMockProfileLinkedCommunication(profileId) + dataSource.communications.value = dataSource.communications.updateAndGet { communications -> + communications.add(decodedCommunication) + communications + } + Result.success(JsonPrimitive("success")) + } + + override suspend fun deleteRemoteTaskById( + profileId: ProfileIdentifier, + taskId: String + ): Result = + withContext(dispatcher) { + dataSource.syncedTasks.value = dataSource.syncedTasks.updateAndGet { syncedList -> + syncedList.removeIf { it.taskId == taskId && it.profileId == profileId } + syncedList + } + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + scannedList + .removeIf { scannedItem -> scannedItem.taskId == taskId && scannedItem.profileId == profileId } + scannedList + } + Result.success(null) + } + + // used only for scanned + override suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + val index = scannedList.indexOfFirst { item -> item.taskId == taskId } + if (index != INDEX_OUT_OF_BOUNDS) { + scannedList[index] = scannedList[index].copy(redeemedOn = timestamp) + } + scannedList + } + } + } + + override suspend fun updateScannedTaskName(taskId: String, name: String) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + val index = scannedList.indexOfFirst { item -> item.taskId == taskId } + if (index != INDEX_OUT_OF_BOUNDS) { + scannedList[index] = scannedList[index].copy(name = name) + } + scannedList + } + } + } + + override fun loadSyncedTaskByTaskId(taskId: String) = + dataSource.syncedTasks.mapNotNull { list -> + list.find { it.taskId == taskId } + }.flowOn(dispatcher) + + override fun loadScannedTaskByTaskId(taskId: String) = + dataSource.scannedTasks.mapNotNull { list -> + list.find { it.taskId == taskId } + }.flowOn(dispatcher) + + override fun loadTaskIds() = + combine( + dataSource.scannedTasks, + dataSource.syncedTasks + ) { scannedTasks, syncedTasks -> + scannedTasks.map { it.taskId }.plus( + syncedTasks.map { it.taskId } + ) + }.flowOn(dispatcher) + + override suspend fun deleteLocalTaskById(taskId: String) { + // do nothing + } + + override suspend fun wasProfileEverAuthenticated(profileId: ProfileIdentifier): Boolean { + return true + } + + override suspend fun redeemScannedTasks(taskIds: List) { + // do nothing + } + + override fun loadAllTaskIds(profileId: ProfileIdentifier): Flow> { + return flowOf(emptyList()) + } + + override suspend fun deleteLocalInvoicesById(taskId: String) { + // do nothing + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockTaskRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockTaskRepository.kt new file mode 100644 index 00000000..c2c78cea --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/prescription/MockTaskRepository.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.repository.prescription + +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +class MockTaskRepository( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : TaskRepository { + override suspend fun downloadTasks(profileId: ProfileIdentifier): Result = + withContext(dispatcher) { + Result.success(0) + } + + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> = Result.success(ResourcePaging.ResourceResult(0, 0)) + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = null +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/profiles/MockProfilesRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/profiles/MockProfilesRepository.kt new file mode 100644 index 00000000..c60248a7 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/profiles/MockProfilesRepository.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.repository.profiles + +import de.gematik.ti.erp.app.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.datasource.MockDataSource +import de.gematik.ti.erp.app.datasource.data.MockProfileInfo.create +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.repository.profiles.MockProfilesRepository.ImageActions.Add +import de.gematik.ti.erp.app.repository.profiles.MockProfilesRepository.ImageActions.NoAction +import de.gematik.ti.erp.app.repository.profiles.MockProfilesRepository.ImageActions.Remove +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +class MockProfilesRepository( + private val dataSource: MockDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : ProfileRepository { + + private fun mockProfiles(): MutableStateFlow> = dataSource.profiles + override fun profiles(): Flow> = mockProfiles() + + override fun activeProfile() = mockProfiles().mapNotNull { + it.find { profile -> profile.active } + } + + override suspend fun saveProfile(profileName: String, activate: Boolean) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + profiles.add(profileName.create()) + profiles + } + } + } + + override suspend fun createNewProfile(profileName: String) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + profiles.add(profileName.create()) + profiles + } + } + } + + override suspend fun activateProfile(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + val updatedProfiles = profiles.replace(profileId = profileId, activate = true) + updatedProfiles + } + } + } + + override suspend fun removeProfile(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profiles -> + profiles.removeIf { profile -> profile.id == profileId } + profiles + } + } + } + + override suspend fun saveInsuranceInformation( + profileId: ProfileIdentifier, + insurantName: String, + insuranceIdentifier: String, + insuranceName: String + ) { + // Not implemented + } + + override suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { + it.replace(profileId = profileId, name = profileName) + } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun updateProfileColor(profileId: ProfileIdentifier, color: ProfilesData.ProfileColorNames) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val updatedList = profileList.replace(profileId = profileId, color = color) + updatedList + } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun updateLastAuthenticated(profileId: ProfileIdentifier, lastAuthenticated: Instant) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, lastAuthenticated = lastAuthenticated) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, avatar = avatar) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, profileImage = profileImage, imageAction = Add) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, profileImage = null, imageAction = Remove) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun switchProfileToPKV(profileId: ProfileIdentifier): Boolean { + // Not implemented for mocks + return false + } + + override suspend fun switchProfileToGKV(profileId: ProfileIdentifier): Boolean { + return true + } + + override suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean = false + + override fun getProfileById(profileId: ProfileIdentifier): Flow = mockProfiles() + .mapNotNull { + it.find { profile -> profile.id == profileId } + } + + override suspend fun isSsoTokenValid(profileId: ProfileIdentifier): Flow { + return flowOf(true) + } + + private fun MutableList.index(profileId: ProfileIdentifier) = + indexOfFirst { profile -> profile.id == profileId } + .takeIf { it != INDEX_OUT_OF_BOUNDS } + + private fun MutableList.replace( + profileId: ProfileIdentifier, + activate: Boolean? = null, + name: String? = null, + color: ProfilesData.ProfileColorNames? = null, + lastAuthenticated: Instant? = null, + avatar: ProfilesData.Avatar? = null, + profileImage: ByteArray? = null, + imageAction: ImageActions = NoAction + ): MutableList = + index(profileId)?.let { index -> + val existingProfile = this[index] + this[index] = this[index].copy( + active = activate ?: existingProfile.active, + name = name ?: existingProfile.name, + color = color ?: existingProfile.color, + lastAuthenticated = lastAuthenticated, + avatar = avatar ?: existingProfile.avatar, + image = when (imageAction) { + Add -> profileImage + Remove -> null + NoAction -> existingProfile.image + } + ) + this + } ?: this + + private fun List.deactivateAllProfiles() = + mapNotNull { + it.copy(active = false) + }.toMutableList() + + private fun MutableList.updateUUIDForChangeVisibility() = + map { it.copy() }.toMutableList() + + enum class ImageActions { + Add, Remove, NoAction + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/usecase/CreateProfileWhenMissingUseCase.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/usecase/CreateProfileWhenMissingUseCase.kt new file mode 100644 index 00000000..a884ce3e --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/usecase/CreateProfileWhenMissingUseCase.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant + +class CreateProfileWhenMissingUseCase( + private val profileRepository: ProfileRepository, + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() { + if (profileRepository.profiles().first().isEmpty()) { + settingsRepository.saveOnboardingData( + authentication = SettingsData.Authentication( + deviceSecurity = false, + password = SettingsData.Authentication.Password("password"), + failedAuthenticationAttempts = 0 + ), + profileName = "Test", + now = fixedInstant() + ) + } + } + + @Suppress("MagicNumber") + private fun fixedInstant(): Instant { + // Define the date and time, October 03, 2023, 00:00:00 + val year = 2023 + val month = 10 + val day = 3 + val hour = 0 // Midnight + val minute = 0 + val second = 0 + + // Create a LocalDateTime + val localDateTime = LocalDateTime(year, month, day, hour, minute, second) + + // Convert LocalDateTime to Instant assuming UTC timezone + return localDateTime.toInstant(TimeZone.UTC) + } +} diff --git a/app/android-mock/src/main/res/resources.properties b/app/android-mock/src/main/res/resources.properties new file mode 100644 index 00000000..fccdea81 --- /dev/null +++ b/app/android-mock/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=de \ No newline at end of file diff --git a/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt b/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt index 4fe4c56e..a1f1d304 100644 --- a/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt +++ b/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt @@ -1,9 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + package de.gematik.ti.erp.app +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * @@ -14,4 +31,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts index 226c0ca1..347ad37a 100644 --- a/app/android/build.gradle.kts +++ b/app/android/build.gradle.kts @@ -1,75 +1,35 @@ +@file:Suppress("VariableNaming", "PropertyName", "UnusedPrivateProperty") -import de.gematik.ti.erp.AppDependenciesPlugin -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import de.gematik.ti.erp.overriding -import org.owasp.dependencycheck.reporting.ReportGenerator.Format +import de.gematik.ti.erp.app.plugins.dependencies.overrides +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin import java.util.Properties plugins { - id("com.android.application") - kotlin("android") - id("org.jetbrains.compose") - id("io.realm.kotlin") - kotlin("plugin.serialization") - id("org.owasp.dependencycheck") - id("com.jaredsburrows.license") - id("de.gematik.ti.erp.dependencies") - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("de.gematik.ti.erp.technical-requirements") + id("base-android-application") + id("de.gematik.ti.erp.dependency-overrides") + id("de.gematik.ti.erp.names") // Release app into play-store id("com.github.triplet.play") version "3.8.6" apply true } -val VERSION_CODE: String by overriding() -val VERSION_NAME: String by overriding() -val TEST_INSTRUMENTATION_ORCHESTRATOR: String? by project - -afterEvaluate { - val taskRegEx = """assemble(Google|Huawei)(PuExternalDebug|PuExternalRelease)""".toRegex() - tasks.forEach { task -> - taskRegEx.matchEntire(task.name)?.let { - val (_, version, flavor) = it.groupValues - task.dependsOn(tasks.getByName("license${version}${flavor}Report")) - } - } -} - -licenseReport { - generateCsvReport = false - generateHtmlReport = false - generateJsonReport = true - copyJsonReportToAssets = true -} +// these two need to be in uppercase since it is declared that way in gradle.properties +val VERSION_CODE: String by overrides() +val VERSION_NAME: String by overrides() +val gematik = AppDependencyNamesPlugin() android { - namespace = AppDependenciesPlugin.APP_NAME_SPACE + namespace = gematik.appNameSpace defaultConfig { - applicationId = AppDependenciesPlugin.APP_ID + applicationId = gematik.appId versionCode = VERSION_CODE.toInt() versionName = VERSION_NAME - testApplicationId = "de.gematik.ti.erp.app.test.test" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - dependencyCheck { - analyzers.assemblyEnabled = false - suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" - formats = listOf(Format.HTML, Format.XML) - scanConfigurations = configurations.filter { - it.name.startsWith("api") || - it.name.startsWith("implementation") || - it.name.startsWith("kapt") - }.map { - it.name - } - } - androidResources { - generateLocaleConfig = true + testApplicationId = gematik.moduleName("test.test") + testInstrumentationRunnerArguments["clearPackageData"] = "true" + testOptions.execution = "ANDROID_TEST_ORCHESTRATOR" + // Check if MAPS_API_KEY is defined, otherwise provide a default value + val mapsApiKey = project.findProperty("MAPS_API_KEY") ?: "DEFAULT_PLACEHOLDER_KEY" + manifestPlaceholders["MAPS_API_KEY"] = mapsApiKey } val rootPath = project.rootProject val signingPropsFile = rootPath.file("signing.properties") @@ -78,18 +38,20 @@ android { val signingProps = Properties() signingProps.load(signingPropsFile.inputStream()) signingConfigs { - fun creatingRelease() = creating { - val target = this.name // property name; e.g. googleRelease - println("Create signing config for: $target") - storeFile = signingProps["$target.storePath"]?.let { - rootPath.file("erp-app-android/$it") + fun creatingRelease() = + creating { + val target = this.name // property name; e.g. googleRelease + println("Create signing config for: $target") + storeFile = + signingProps["$target.storePath"]?.let { + rootPath.file("erp-app-android/$it") + } + println("\tstore: ${signingProps["$target.storePath"]}") + keyAlias = signingProps["$target.keyAlias"] as? String + println("\tkeyAlias: ${signingProps["$target.keyAlias"]}") + storePassword = signingProps["$target.storePassword"] as? String + keyPassword = signingProps["$target.keyPassword"] as? String } - println("\tstore: ${signingProps["$target.storePath"]}") - keyAlias = signingProps["$target.keyAlias"] as? String - println("\tkeyAlias: ${signingProps["$target.keyAlias"]}") - storePassword = signingProps["$target.storePassword"] as? String - keyPassword = signingProps["$target.keyPassword"] as? String - } if (signingProps["googleRelease.storePath"] != null) { val googleRelease by creatingRelease() } @@ -100,14 +62,13 @@ android { } else { println("No signing properties found!") } - buildTypes { val release by getting { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) if (signingPropsFile.canRead()) { signingConfig = signingConfigs.getByName("googleRelease") @@ -116,27 +77,32 @@ android { } val debug by getting { applicationIdSuffix = ".test" - resValue("string", "app_label", "E-Rezept Debug") versionNameSuffix = "-debug" - signingConfigs { - getByName("debug") { - storeFile = rootPath.file("keystore/debug.keystore") - keyAlias = "androiddebugkey" - storePassword = "android" - keyPassword = "android" + resValue("string", "app_label", "E-Rezept Debug") + if(rootPath.file("keystore/debug.keystore").exists()) { // needed tp be able to build on github + signingConfigs { + getByName("debug") { + storeFile = rootPath.file("keystore/debug.keystore") + keyAlias = "androiddebugkey" + storePassword = "android" + keyPassword = "android" + } } } } - create("minifiedDebug") { - initWith(debug) + create(gematik.minifiedDebug) { applicationIdSuffix = ".minirelease" + isDebuggable = true isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) - resValue("string", "app_label", "E-Rezept MiniRelease") + if (signingPropsFile.canRead()) { + signingConfig = signingConfigs.getByName("googleRelease") + } + resValue("string", "app_label", "E-Rezept Mini") } } flavorDimensions += listOf("version") @@ -162,6 +128,7 @@ android { applicationIdSuffix = ".konnektathon.ru" versionNameSuffix = "-konnektathon-RU" signingConfig = signingConfigs.findByName("googleRelease") + resValue("string", "app_label", "E-Rezept Konny") } } if (flavor?.startsWith("konnektathonDevru") == true) { @@ -170,137 +137,38 @@ android { applicationIdSuffix = ".konnektathon.rudev" versionNameSuffix = "-konnektathon-RUDEV" signingConfig = signingConfigs.findByName("googleRelease") + resValue("string", "app_label", "E-Rezept Konny Dev") } } } - packagingOptions { - resources { - excludes += "META-INF/**" - // for JNA and JNA-platform - excludes += "META-INF/AL2.0" - excludes += "META-INF/LGPL2.1" - // for byte-buddy - excludes += "META-INF/licenses/ASM" - pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" - pickFirsts += "win32-x86/attach_hotspot_windows.dll" - } + testOptions { + animationsDisabled = true } } dependencies { - implementation(project(":app:features")) - implementation(project(":app:demo-mode")) - androidTestImplementation(project(":app:shared-test")) - implementation(project(":common")) - testImplementation(project(":common")) - testImplementation(kotlin("test")) - implementation("com.tom-roush:pdfbox-android:2.0.27.0") { - exclude(group = "org.bouncycastle") - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) - } + implementation(project(gematik.feature)) + implementation(project(gematik.demoMode)) + implementation(project(gematik.uiComponents)) + androidTestImplementation(project(gematik.testActions)) + androidTestImplementation(project(gematik.testTags)) + implementation(project(gematik.multiplatform)) + testImplementation(project(gematik.multiplatform)) + implementation(libs.play.app.update) + implementation(libs.tracing) + debugImplementation(libs.tracing) - inject { - dateTime { - implementation(datetime) - testCompileOnly(datetime) - } - android { - coreLibraryDesugaring(desugaring) - debugImplementation(processPhoenix) - } - androidX { - implementation(appcompat) - implementation(composeNavigation) - implementation(security) - implementation(lifecycleViewmodel) - implementation(lifecycleProcess) - implementation(lifecycleComposeRuntime) - } - dependencyInjection { - compileOnly(kodeinCompose) - androidTestImplementation(kodeinCompose) - } - logging { - implementation(napier) - } - tracking { - implementation(contentSquare) - } - compose { - implementation(runtime) - implementation(foundation) - implementation(uiTooling) - implementation(preview) - } - crypto { - testImplementation(jose4j) - testImplementation(bouncycastleBcprov) - testImplementation(bouncycastleBcpkix) - } - database { - testCompileOnly(realm) - } - network { - implementation(retrofit) - implementation(retrofit2KotlinXSerialization) - implementation(okhttp3) - implementation(okhttpLogging) - // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). - // Can be removed as soon as Retrofit releases a new version >2.9.0. - implementation(okio) + androidTestImplementation(libs.kodeon.core) + androidTestImplementation(libs.kodeon.android) + androidTestImplementation(libs.primsys.client) +} - androidTestImplementation(okhttp3) - } - database { - compileOnly(realm) - testCompileOnly(realm) - } - playServices { - implementation(appUpdate) - } - serialization { - implementation(kotlinXJson) - } - androidXTest { - testImplementation(archCore) - androidTestImplementation(core) - androidTestImplementation(rules) - androidTestImplementation(junitExt) - androidTestImplementation(runner) - androidTestUtil(orchestrator) - androidTestUtil(services) - // androidTestImplementation(navigation) - androidTestImplementation(espresso) - androidTestImplementation(espressoIntents) - } - tracing { - debugImplementation(tracing) - implementation(tracing) - } - coroutinesTest { - testImplementation(coroutinesTest) - } - composeTest { - androidTestImplementation(ui) - debugImplementation(uiManifest) - androidTestImplementation(junit4) - } - networkTest { - testImplementation(mockWebServer) - } - test { - testImplementation(junit4) - testImplementation(snakeyaml) - testImplementation(json) - testImplementation(mockkOld) - androidTestImplementation(mockkAndroid) - } +// keep this here since it cannot be changed for mock app +configurations.all { + resolutionStrategy { + force("io.netty:netty-codec-http2:4.1.100.Final") + force("com.google.protobuf:protobuf-java:4.28.2") } } -secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) "ci-overrides.properties" else "gradle.properties" -} diff --git a/app/android/proguard-rules.pro b/app/android/proguard-rules.pro index d159bfe9..4680d16a 100644 --- a/app/android/proguard-rules.pro +++ b/app/android/proguard-rules.pro @@ -128,6 +128,9 @@ -keep,allowobfuscation,allowshrinking class okhttp3.RequestBody -keep,allowobfuscation,allowshrinking class okhttp3.ResponseBody +# app classes +-keep class de.gematik.ti.erp.app.debugsettings.** { *; } + # With R8 full mode generic signatures are stripped for classes that are not # kept. Suspend functions are wrapped in continuations where the type argument # is used. @@ -139,5 +142,9 @@ -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** +# Keep all classes, methods, and fields in the com.appmattus.certificatetransparency package +-keep class com.appmattus.certificatetransparency.** { *; } +-keep class com.appmattus.certificatetransparency.**$* { *; } + -printconfiguration "~/tmp/full-r8-config.txt" diff --git a/app/android/src/androidTest/AndroidManifest.xml b/app/android/src/androidTest/AndroidManifest.xml deleted file mode 100644 index 4e10e648..00000000 --- a/app/android/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt deleted file mode 100644 index 9b40c15b..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("ktlint:max-line-length") - -package de.gematik.ti.erp.app.test.test - -object TestConfig { - const val WeakPassword = "TrustNo1" - const val StrongPassword = "Jaja Ding Dong!" - const val DefaultProfileName = "Rainer Reizdarm" - const val DefaultEGKCAN = "123123" - const val DefaultEGKPassword = "123456" - const val WaitTimeout1Sec = 1_000L - const val ScreenChangeTimeout = 3_000L - const val LoadPrescriptionsTimeout = 20_000L - - const val AppDefaultVirtualEgkKvnr = "X764228532" - const val PharmacyName = "Apotheke am Flughafen - E2E-Test" - const val PharmacyTelematikId = "3-SMC-B-Testkarte-883110000116873" - - const val PharmacyZoti01 = "ZoTI_01_TEST-ONLY" - const val PharmacyZoti02 = "ZoTI_02_TEST-ONLY" - const val PharmacyZoti03 = "ZoTI_03_TEST-ONLY" - const val PharmacyZoti04 = "ZoTI_04_TEST-ONLY" - const val PharmacyZoti05 = "ZoTI_05_TEST-ONLY" - const val PharmacyZoti06 = "ZoTI_06_TEST-ONLY" - const val PharmacyZoti07 = "ZoTI_07_TEST-ONLY" - const val PharmacyZoti08 = "ZoTI_08_TEST-ONLY" - const val PharmacyZoti09 = "ZoTI_09_TEST-ONLY" - const val PharmacyZoti10 = "ZoTI_10_TEST-ONLY" - const val PharmacyZoti11 = "ZoTI_11_TEST-ONLY" - const val PharmacyZoti12 = "ZoTI_12_TEST-ONLY" - const val PharmacyZoti13 = "ZoTI_13_TEST-ONLY" - const val PharmacyZoti14 = "ZoTI_14_TEST-ONLY" - const val PharmacyZoti15 = "ZoTI_15_TEST-ONLY" - const val PharmacyZoti16 = "ZoTI_16_TEST-ONLY" - const val PharmacyZoti17 = "ZoTI_17_TEST-ONLY" - const val PharmacyZoti18 = "ZoTI_18_TEST-ONLY" - const val PharmacyZoti19 = "ZoTI_19_TEST-ONLY" - const val PharmacyZoti20 = "ZoTI_20_TEST-ONLY" - - object FD { - const val DefaultServer = "https://erpps-test.dev.gematik.solutions" - const val DefaultDoctor = "9a15b6f9f4b8f2e9df1db745a4091bbd" - const val DefaultPharmacy = "886c6eda7dd5a1c6b1d112907f544d3" - } -} - -interface VirtualEgk { - val certificate: String - val privateKey: String - val kvnr: String - val name: String -} - -object VirtualEgk1 : VirtualEgk { - @Suppress("MaxLineLength") - override val certificate = - "MIIDXTCCAwSgAwIBAgIHAs9vZEwB8jAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAxMjQwMDAwMDBaFw0yNzAxMjMyMzU5NTlaMIHgMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDU5Mjk3MTEUMBIGA1UEBAwLVsOzcm13aW5rZWwxHDAaBgNVBCoME1hlbmlhIFZlcmEgQWRlbGhlaWQxEjAQBgNVBAwMCVByb2YuIERyLjE1MDMGA1UEAwwsUHJvZi4gRHIuIFhlbmlhIFZlcmEgQS4gVsOzcm13aW5rZWxURVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAEczsMfajcnKpGYyNeXUhODjyrX4z4j9Qzio/Ulq5COPVySk0CxYBDj+1VEd5FalhEJXC9HjVRCflQx+RkEQFbvqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBTCDfBZ8X30CZnFk7E2x8+lMM5uODA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIDDAXcyOKDYOZpoH0iYijr1yisyxHeT3ch6XZlFNXPrKAiAHepW4TOQAoqyoGG9Pgly0TO2tTB7WLKEc7B3F6lNhpA==" - override val privateKey = "AJzshqeIuhwReqZpWbqY0PnRjTdTRzk4Zj9GpSxcUukA" - override val kvnr = "X110592971" - override val name = "Vórmwinkel Xenia Vera Adelheid" -} - -object VirtualEgkWithPrescription : VirtualEgk { - @Suppress("MaxLineLength") - override val certificate = - "MIIDLTCCAtSgAwIBAgIHAZ/zfVKUfTAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAyMDQwMDAwMDBaFw0yNzAyMDMyMzU5NTlaMIGwMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDUzNTU0MTEOMAwGA1UEBAwFS2zDtm4xDTALBgNVBCoMBEx1Y2ExDDAKBgNVBAwMA0RyLjEgMB4GA1UEAwwXRHIuIEx1Y2EgS2zDtm5URVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAETn/MKYxsnBH9khicaXG3mFc5v4RoL0ILuJ3TreTsiFsv91OA6Yj/O4EAxm6dCpPtGgWRyVUYbOgDkaDurSUPpqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBRhIkfxtBhE+Z3fcu+OWu/3gnnYqjA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIGHDnSVg2A9NmFPhtzo4dL3CVbN94k3NrYhXLOZoCUFXAiBlE6TfW6uL91jhv8JuupHhr7X6B9YcbVizWoMxo1grFA==" - override val privateKey = "cv2z1KGMJi+M5foz3GCz0bi5pSdBIjVTqw2cUuIsJcY=" - override val kvnr = "X110535541" - override val name = "Klön Luca" -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt deleted file mode 100644 index 2f5e22d9..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test - -import de.gematik.ti.erp.app.test.test.core.execShellCmd -import org.junit.After -import org.junit.AfterClass -import org.junit.Before -import org.junit.BeforeClass -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -open class WithFontScale(protected val fontScale: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{index}: fontScale={0}") - fun data(): Collection> { - return listOf( - arrayOf("1.0"), - arrayOf("1.3") - ) - } - - @JvmStatic - @BeforeClass - fun disableGestureNavbar() { - execShellCmd("cmd overlay disable com.android.internal.systemui.navbar.gestural") - } - - @JvmStatic - @AfterClass - fun enableGestureNavbar() { - execShellCmd("cmd overlay enable com.android.internal.systemui.navbar.gestural") - } - } - - @Before - fun changeScales() { - execShellCmd("settings put system font_scale $fontScale") - } - - @After - fun resetScales() { - execShellCmd("settings put system font_scale 1.0") - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt deleted file mode 100644 index 1e1520a9..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.compose - -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextClearance -import androidx.compose.ui.test.performTextReplacement -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.EditableTextField -import de.gematik.ti.erp.app.utils.compose.ErrorTextTag -import io.github.aakira.napier.Napier -import org.junit.Rule -import org.junit.Test - -class EditableTextFieldTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun testEditableTextField() { - val text = "some random text" - val newText = "some new text" - - composeTestRule.setContent { - AppTheme { - EditableTextField( - text = text, - textMinLength = 20, - onDoneClicked = { - Napier.d { "text is $it" } - } - ) - } - } - - // test existing text - composeTestRule.onNodeWithText(text).assertIsDisplayed() - composeTestRule.onNodeWithText(text).performClick() - composeTestRule.onNodeWithText(text).assertHasClickAction() - - // test text change - composeTestRule.onNodeWithText(text).performTextReplacement(newText) - composeTestRule.onNodeWithText(newText).assertIsDisplayed() - - // test showing error text - composeTestRule.onNodeWithText(newText).performTextClearance() - composeTestRule.onNodeWithTag(ErrorTextTag).assertIsDisplayed() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt deleted file mode 100644 index 1c9aca52..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core - -import android.util.Log -import de.gematik.ti.erp.app.TestWrapper -import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox -import de.gematik.ti.erp.app.test.test.core.prescription.Prescription -import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionDoctorUseCase -import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionPharmacyUseCase -import de.gematik.ti.erp.app.test.test.core.prescription.Task -import de.gematik.ti.erp.app.test.test.core.prescription.retry -import org.junit.Assume -import org.junit.AssumptionViolatedException - -private val doctorUseCase by lazy { PrescriptionDoctorUseCase() } -private val pharmacyUseCase by lazy { PrescriptionPharmacyUseCase() } - -class TaskCollection( - data: List, - private val testWrapper: TestWrapper -) { - data class TaskAndPrescription( - val task: Task, - val prescription: Prescription, - val isDispensed: Boolean = false - ) - - private val _taskData = mutableListOf() - .apply { - addAll(data) - } - - val taskData: List - get() = _taskData - - fun deleteAll() { - // clean up - allowed to throw, so test won't fail - taskData.forEach { (task, _, isDispensed) -> - if (task.secret == null || isDispensed) { - testWrapper.deleteTask(task.taskId) - } else { - runCatching { - pharmacyUseCase - .abortTask(taskId = task.taskId, accessCode = task.accessCode, secret = task.secret) - } - } - .onFailure { Log.e("deleteTask", "Deleting task with id `${task.taskId}` failed", it) } - .onSuccess { Log.d("deleteTask", "Task with id `${task.taskId}` deleted") } - } - } - - fun accept(data: TaskAndPrescription): Task { - val taskWithSecret = pharmacyUseCase.acceptTask(taskId = data.task.taskId, accessCode = data.task.accessCode) - requireNotNull(taskWithSecret.secret) { "Accepting a task must return secret!" } - - _taskData.replaceWith(data.copy(task = taskWithSecret)) - - return taskWithSecret - } - - fun reply(data: TaskAndPrescription, message: CommunicationPayloadInbox) { - pharmacyUseCase.replyWithCommunication( - taskId = data.task.taskId, - kvNr = data.prescription.patient!!.kvnr!!, - message = message - ) - } - - fun dispense(data: TaskAndPrescription) { - requireNotNull(data.task.secret) { "Task can be only dispensed with the `secret`" } - pharmacyUseCase.dispenseMedication( - taskId = data.task.taskId, - accessCode = data.task.accessCode, - secret = data.task.secret - ) - _taskData.replaceWith(data.copy(isDispensed = true)) - } - - companion object { - fun generate(count: Int, kvnr: String, testWrapper: TestWrapper): TaskCollection { - require(count >= 1) - - val taskData = (1..count).mapNotNull { - retry(3) { doctorUseCase.prescribeToPatient(kvnr) } - } - val prescriptions = taskData.mapNotNull { data -> - retry(3) { doctorUseCase.prescriptionDetails(data.taskId) } - } - - try { - Assume.assumeTrue( - "Prescription server couldn't create all tasks", - taskData.size == prescriptions.size && taskData.size == count - ) - } catch (e: AssumptionViolatedException) { - taskData.forEach { data -> - val taskId = data.taskId - testWrapper.deleteTask(taskId) - .onFailure { Log.e("deleteTask", "Deleting task with id `$taskId` failed", it) } - .onSuccess { Log.d("deleteTask", "Task with id `$taskId` deleted") } - } - throw e - } - - val data = taskData.zip(prescriptions) { tD: Task, p: Prescription -> - TaskAndPrescription( - task = tD, - prescription = p - ) - } - - Log.d("generate", "Prescriptions:\n${prescriptions.joinToString("\n\n") { it.toString() }}") - - return TaskCollection( - data, - testWrapper - ) - } - } -} - -private fun MutableList.replaceWith(data: TaskCollection.TaskAndPrescription) { - val ix = indexOfFirst { it.task.taskId == data.task.taskId } - require(ix != -1) - this[ix] = data -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt deleted file mode 100644 index 630f2f6e..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core - -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.SemanticsNodeInteractionCollection -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.printToString -import androidx.test.platform.app.InstrumentationRegistry -import de.gematik.ti.erp.app.InsuranceState -import de.gematik.ti.erp.app.MedicationCategory -import de.gematik.ti.erp.app.PharmacyId -import de.gematik.ti.erp.app.PrescriptionId -import de.gematik.ti.erp.app.SubstitutionAllowed -import de.gematik.ti.erp.app.SupplyForm - -fun ComposeTestRule.awaitDisplay(timeout: Long, vararg tags: String): String { - val t0 = System.currentTimeMillis() - do { - tags.forEach { tag -> - try { - onNodeWithTag(tag).assertIsDisplayed() - return tag - } catch (_: AssertionError) { - } - } - mainClock.advanceTimeBy(100) - Thread.sleep(100) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.awaitDisplay(timeout: Long, node: () -> SemanticsNodeInteraction) { - val t0 = System.currentTimeMillis() - do { - try { - node().assertIsDisplayed() - return - } catch (_: AssertionError) { - } - mainClock.advanceTimeBy(100) - Thread.sleep(100) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.await(timeout: Long, node: () -> Unit) { - val t0 = System.currentTimeMillis() - do { - try { - node() - return - } catch (_: AssertionError) { - } - mainClock.advanceTimeBy(100) - Thread.sleep(100) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.sleep(timeout: Long) { - val t0 = System.currentTimeMillis() - do { - mainClock.advanceTimeBy(100) - Thread.sleep(100) - } while (System.currentTimeMillis() - t0 < timeout) -} - -fun SemanticsNodeInteraction.assertHasText(includeEditableText: Boolean = true) = - assert(hasText(includeEditableText)) - -fun hasText( - includeEditableText: Boolean = true -): SemanticsMatcher { - val propertyName = if (includeEditableText) { - "${SemanticsProperties.Text.name} + ${SemanticsProperties.EditableText.name}" - } else { - SemanticsProperties.Text.name - } - return SemanticsMatcher( - propertyName - ) { node -> - val actual = mutableListOf() - if (includeEditableText) { - node.config.getOrNull(SemanticsProperties.EditableText) - ?.let { actual.add(it.text) } - } - node.config.getOrNull(SemanticsProperties.Text) - ?.let { actual.addAll(it.map { anStr -> anStr.text }) } - actual.all { it.isNotBlank() } - } -} - -fun SemanticsNodeInteractionCollection.assertNone( - matcher: SemanticsMatcher -): SemanticsNodeInteractionCollection = - filter(matcher) - .assertCountEquals(0) - -fun hasPrescriptionId(id: String): SemanticsMatcher = - SemanticsMatcher.expectValue(PrescriptionId, id) - -fun hasPharmacyId(id: String): SemanticsMatcher = - SemanticsMatcher.expectValue(PharmacyId, id) - -fun hasInsuranceState(state: String?): SemanticsMatcher = - SemanticsMatcher.expectValue(InsuranceState, state) - -fun hasSubstitutionAllowed(allowed: Boolean): SemanticsMatcher = - SemanticsMatcher.expectValue(SubstitutionAllowed, allowed) - -fun hasSupplyForm(form: String): SemanticsMatcher = - SemanticsMatcher.expectValue(SupplyForm, form) - -fun hasMedicationCategory(form: String): SemanticsMatcher = - SemanticsMatcher.expectValue(MedicationCategory, form) - -fun execShellCmd(cmd: String) { - InstrumentationRegistry.getInstrumentation().uiAutomation - .executeShellCommand(cmd) -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt deleted file mode 100644 index 5dacac56..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -enum class SupplyOptionsType { - @SerialName("shipment") - Shipment, - - @SerialName("onPremise") - OnPremise, - - @SerialName("delivery") - Delivery -} - -@Serializable -data class CommunicationPayloadInbox( - val version: String = "1", - val supplyOptionsType: SupplyOptionsType, - @SerialName("info_text") val infoText: String? = null, - val url: String? = null, - val pickUpCodeHR: String? = null, - val pickUpCodeDMC: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt deleted file mode 100644 index 4bbf6ac2..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Coverage( - @SerialName("iknr") - val iknr: String? = null, - @SerialName("insuranceKind") - val insuranceKind: String? = null, - @SerialName("insuranceName") - val insuranceName: String? = null, - @SerialName("insuranceState") - val insuranceState: String? = null, - @SerialName("payorType") - val payorType: String? = null, - @SerialName("personGroup") - val personGroup: String? = null, - @SerialName("wop") - val wop: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt deleted file mode 100644 index 66090324..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Medication( - @SerialName("amount") - val amount: Int? = null, - @SerialName("category") - val category: String? = null, - @SerialName("dosage") - val dosage: String? = null, - @SerialName("freeText") - val freeText: String? = null, - @SerialName("ingredient") - val ingredient: String? = null, - @SerialName("ingredientStrength") - val ingredientStrength: String? = null, - @SerialName("name") - val name: String? = null, - @SerialName("packageQuantity") - val packageQuantity: Int? = null, - @SerialName("pzn") - val pzn: String? = null, - @SerialName("standardSize") - val standardSize: String? = null, - @SerialName("substitutionAllowed") - val substitutionAllowed: Boolean? = null, - @SerialName("supplyForm") - val supplyForm: String? = null, - @SerialName("type") - val type: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt deleted file mode 100644 index 554cd04f..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Patient( - @SerialName("birthDate") - val birthDate: String? = null, - @SerialName("city") - val city: String? = null, - @SerialName("firstName") - val firstName: String? = null, - @SerialName("kvnr") - val kvnr: String? = null, - @SerialName("lastName") - val lastName: String? = null, - @SerialName("postal") - val postal: String? = null, - @SerialName("street") - val street: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt deleted file mode 100644 index 37fdd884..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Practitioner( - @SerialName("bsnr") - val bsnr: String? = null, - @SerialName("city") - val city: String? = null, - @SerialName("email") - val email: String? = null, - @SerialName("hba") - val hba: String? = null, - @SerialName("id") - val id: String? = null, - @SerialName("lanr") - val lanr: String? = null, - @SerialName("name") - val name: String? = null, - @SerialName("officeName") - val officeName: String? = null, - @SerialName("phone") - val phone: String? = null, - @SerialName("postal") - val postal: String? = null, - @SerialName("smcb") - val smcb: String? = null, - @SerialName("street") - val street: String? = null, - @SerialName("ti") - val telematik: Telematik? = null, - @SerialName("type") - val type: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt deleted file mode 100644 index 3235cbaf..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Prescription( - @SerialName("accessCode") - val accessCode: String? = null, - @SerialName("authoredOn") - val authoredOn: Long? = null, - @SerialName("authoredOnFormatted") - val authoredOnFormatted: String? = null, - @SerialName("coverage") - val coverage: Coverage? = null, - @SerialName("medication") - val medication: Medication? = null, - @SerialName("patient") - val patient: Patient? = null, - @SerialName("practitioner") - val practitioner: Practitioner? = null, - @SerialName("prescriptionId") - val prescriptionId: String? = null, - @SerialName("taskId") - val taskId: String = "" -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt deleted file mode 100644 index 96f81d6a..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import android.util.Log -import de.gematik.ti.erp.app.test.test.TestConfig -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody - -private fun defaultPayload(kvnr: String) = """ -{ - "patient": { - "kvnr": "$kvnr" - }, - "medication": { - "category": "00" - } -} -""" - -class PrescriptionDoctorUseCase { - private val okHttp by lazy { OkHttpClient.Builder().build() } - - private val json = Json { - encodeDefaults = false - ignoreUnknownKeys = true - } - - fun prescribeToPatient(kvNr: String): Task { - val payload = defaultPayload(kvNr) - - val response = okHttp.newCall( - Request.Builder() - .url("${TestConfig.FD.DefaultServer}/doc/${TestConfig.FD.DefaultDoctor}/prescribe") - .post(payload.toRequestBody("application/json".toMediaType())) - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - - val body = requireNotNull(response.body).string() - Log.d("prescribeToPatient", body) - - return json.decodeFromString(body) - } - - fun prescriptionDetails(taskId: String): Prescription { - val response = okHttp.newCall( - Request.Builder() - .url("${TestConfig.FD.DefaultServer}/prescription/$taskId") - .get() - .build() - ).execute() - - require(response.isSuccessful) - - val body = requireNotNull(response.body).string() - Log.d("prescriptionDetails", body) - - return json.decodeFromString(body) - } -} - -fun retry(n: Int, block: () -> T): T? { - var retriesLeft = n - while (retriesLeft > 0) { - try { - return block() - } catch (e: Exception) { - Log.e("retry", "Reason: ", e) - retriesLeft-- - } - } - return null -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt deleted file mode 100644 index 5a4ef7c5..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import android.util.Log -import de.gematik.ti.erp.app.test.test.TestConfig -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody - -class PrescriptionPharmacyUseCase { - private val okHttp by lazy { OkHttpClient.Builder().build() } - - private val json = Json { - encodeDefaults = false - ignoreUnknownKeys = true - } - fun rejectTask(taskId: String, accessCode: String, secret: String) { - val response = okHttp.newCall( - @Suppress("ktlint:max-line-length", "MaxLineLength") - Request.Builder() - .url( - "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/reject/?taskId=$taskId&ac=$accessCode&secret=$secret" - ) - .post("".toRequestBody()) - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - } - - fun acceptTask(taskId: String, accessCode: String): Task { - val response = okHttp.newCall( - @Suppress("ktlint:max-line-length", "MaxLineLength") - Request.Builder() - .url( - "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/accept/?taskId=$taskId&ac=$accessCode" - ) - .post("".toRequestBody()) - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - - val body = requireNotNull(response.body).string() - Log.d("acceptTask", body) - - return json.decodeFromString(body) - } - - fun abortTask(taskId: String, accessCode: String, secret: String) { - val response = okHttp.newCall( - @Suppress("ktlint:max-line-length", "MaxLineLength") - Request.Builder() - .url( - "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/abort/?taskId=$taskId&ac=$accessCode&secret=$secret" - ) - .delete() - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - } - - fun replyWithCommunication(taskId: String, kvNr: String, message: CommunicationPayloadInbox) { - val supplyOption = when (message.supplyOptionsType) { - SupplyOptionsType.Shipment -> "shipment" - SupplyOptionsType.OnPremise -> "onPremise" - SupplyOptionsType.Delivery -> "delivery" - } - - val response = okHttp.newCall( - @Suppress("ktlint:max-line-length", "MaxLineLength") - Request.Builder() - .url( - "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/reply/?taskId=$taskId&kvnr=$kvNr&supplyOption=$supplyOption" - ) - .post(json.encodeToString(message).toRequestBody()) - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - } - - fun dispenseMedication(taskId: String, accessCode: String, secret: String) { - val response = okHttp.newCall( - @Suppress("ktlint:max-line-length", "MaxLineLength") - Request.Builder() - .url( - "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/close/?taskId=$taskId&ac=$accessCode&secret=$secret" - ) - .delete() - .build() - ).execute() - - require(response.isSuccessful) { "Response was: $response" } - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt deleted file mode 100644 index ecea4731..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.test.ext.junit.rules.ActivityScenarioRule -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.core.sleep -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen - -class PrescriptionUtils( - private val composeRule: AndroidComposeTestRule, MainActivity>, - private val mainScreen: MainScreen, - private val prescriptionsScreen: PrescriptionsScreen -) { - fun loginWithVirtualHealthCardFromMainScreen() { - mainScreen.userSeesMainScreen() - composeRule.activity.testWrapper.loginWithVirtualHealthCard() - composeRule.sleep(1_000L) - prescriptionsScreen.refreshPrescriptions() - } - - fun deleteAllPrescriptions() { - composeRule.activity.testWrapper.deleteAllTasksSafe() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt deleted file mode 100644 index 64155211..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonNames - -@Serializable -data class Task -@OptIn(ExperimentalSerializationApi::class) -constructor( - @SerialName("task-id") - val taskId: String, - @JsonNames("access-code", "accessCode") - val accessCode: String, - @SerialName("secret") - val secret: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt deleted file mode 100644 index 49bbaabb..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.core.prescription - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Telematik( - @SerialName("discoveryDocument") - val discoveryDocument: String? = null, - @SerialName("fachdienst") - val fachdienst: String? = null, - @SerialName("tsl") - val tsl: String? = null -) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt deleted file mode 100644 index 9d29e0af..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.functions - -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.platform.app.InstrumentationRegistry -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.ui.PrescriptionStateInfo -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.toStartOfDayInUTC -import kotlinx.datetime.Clock -import org.junit.Rule -import org.junit.Test -import kotlin.time.Duration.Companion.days - -class ReadyPrescriptionStateInfoTest { - @get:Rule - val composeTestRule = createComposeRule() - - private val context = InstrumentationRegistry.getInstrumentation().targetContext - private val now = Clock.System.now() - - @Test - fun syncedTaskReadyState_27AcceptDaysLeftTest() { - val acceptUntil = now.plus(27.days).toStartOfDayInUTC() - val expiresOn = now.plus(90.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = String.format(context.resources.getString(R.string.prescription_item_accept_days_left), "27") - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_2AcceptDaysLeftTest() { - val acceptUntil = now.plus(2.days).toStartOfDayInUTC() - val expiresOn = now.plus(65.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + - String.format(context.resources.getString(R.string.prescription_item_two_accept_days_left), "2") - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_1AcceptDaysLeftTest() { - val acceptUntil = now.plus(1.days).toStartOfDayInUTC() - val expiresOn = now.plus(64.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + - context.resources.getString(R.string.prescription_item_accept_only_tomorrow) - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_0AcceptDaysLeftTest() { - val acceptUntil = now.toStartOfDayInUTC() - val expiresOn = now.plus(63.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = String.format(context.resources.getString(R.string.prescription_item_warning_amber)) + - context.resources.getString(R.string.prescription_item_accept_only_today) - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_62ExpiryDaysLeftTest() { - val acceptUntil = now.minus(1.days).toStartOfDayInUTC() - val expiresOn = now.plus(62.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = - String.format(context.resources.getString(R.string.prescription_item_expiration_days_left), "62") - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_2ExpiryDaysLeftTest() { - val acceptUntil = now.minus(61.days).toStartOfDayInUTC() - val expiresOn = now.plus(2.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + - String.format(context.resources.getString(R.string.prescription_item_two_expiration_days_left), "2") - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_1ExpiryDaysLeftTest() { - val acceptUntil = now.minus(62.days).toStartOfDayInUTC() - val expiresOn = now.plus(1.days).toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + - context.resources.getString(R.string.prescription_item_expiration_only_tomorrow) - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } - - @Test - fun syncedTaskReadyState_0ExpiryDaysLeftTest() { - val acceptUntil = now.minus(63.days).toStartOfDayInUTC() - val expiresOn = now.toStartOfDayInUTC() - val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) - val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + - context.resources.getString(R.string.prescription_item_expiration_only_today) - composeTestRule.setContent { - AppTheme { - PrescriptionStateInfo(state = readyState) - } - } - composeTestRule.onNode(hasText(expectedText)).assertExists() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt deleted file mode 100644 index 2da55556..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen -import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen -import org.junit.Assume -import org.junit.Rule -import org.junit.Test - -class Cleanup { - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } - private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } - - @Test - fun not_a_test_cleanup() { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - prescriptionsScreen.awaitPrescriptions() - prescriptionUtils.deleteAllPrescriptions() - Assume.assumeTrue(false) - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt deleted file mode 100644 index 7ece16ee..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.LargeTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.steps.CardWallScreenSteps -import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps -import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps -import de.gematik.ti.erp.app.test.test.steps.SettingScreenSteps -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@LargeTest -class HealthCardOrder(fontScale: String) : WithFontScale(fontScale) { - - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingSteps by lazy { OnboardingSteps(composeRule) } - private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } - private val cardWallScreenSteps by lazy { CardWallScreenSteps(composeRule) } - - private val settingScreenSteps by lazy { SettingScreenSteps(composeRule) } - - @Before - fun skipsOnboarding() { - onboardingSteps.userSkipsOnboarding() - } - - @After - fun destroyActivity() { - composeRule.activity.finish() - } - - @Test - fun list_health_insurance_company() { - mainScreenSteps.userTapsSettingsMenuButton() - settingScreenSteps.userWantsToOrderNewCard() - settingScreenSteps.userSeesAListOfInsurances() - } - - @Test - fun ordering_new_EGKFromSettings() { - mainScreenSteps.userTapsSettingsMenuButton() - settingScreenSteps.userWantsToOrderNewCard() - settingScreenSteps.userSeesAListOfInsurances() - settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") - settingScreenSteps.userSeesOrderOptionScreen() - settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") - settingScreenSteps.userSeesHealthCardOrderContactScreen() - settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") - } - - @Test - fun ordering_new_EGKFromIntroScreen() { - cardWallScreenSteps.fakeNFCCapabilities() - mainScreenSteps.userTapsConnect() - cardWallScreenSteps.userClicksOrderHealthCardFromCardWallIntroScreen() - settingScreenSteps.userSeesAListOfInsurances() - settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") - settingScreenSteps.userSeesOrderOptionScreen() - settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") - settingScreenSteps.userSeesHealthCardOrderContactScreen() - settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") - } - - @Test - fun ordering_new_EGKFromCANScreen() { - cardWallScreenSteps.fakeNFCCapabilities() - mainScreenSteps.userTapsConnect() - cardWallScreenSteps.userClicksOrderHealthCardFromCardWallCANScreen() - settingScreenSteps.userSeesAListOfInsurances() - settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") - settingScreenSteps.userSeesOrderOptionScreen() - settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") - settingScreenSteps.userSeesHealthCardOrderContactScreen() - settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") - } - - @Test - fun ordering_new_EGKFromPINScreen() { - cardWallScreenSteps.fakeNFCCapabilities() - mainScreenSteps.userTapsConnect() - cardWallScreenSteps.userClicksOrderHealthCardFromCardWallPinScreen() - settingScreenSteps.userSeesAListOfInsurances() - settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") - settingScreenSteps.userSeesOrderOptionScreen() - settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") - settingScreenSteps.userSeesHealthCardOrderContactScreen() - settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") - } - - @Test - fun cancel_ordering_new_EGK() { - mainScreenSteps.userTapsSettingsMenuButton() - settingScreenSteps.userWantsToOrderNewCard() - settingScreenSteps.userSeesAListOfInsurances() - settingScreenSteps.userAbortsOrderingOfNewCard() - settingScreenSteps.userSeesSettingsScreen() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfileScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfileScreen.kt deleted file mode 100644 index f69be2e3..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfileScreen.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.MediumTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps -import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps -import de.gematik.ti.erp.app.test.test.steps.ProfileSettingsSteps -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@MediumTest -class MultiProfileScreen(fontScale: String) : WithFontScale(fontScale) { - - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingSteps by lazy { OnboardingSteps(composeRule) } - private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } - - private val profileSettingSteps by lazy { ProfileSettingsSteps(composeRule) } - - @Before - fun skipsOnboarding() { - onboardingSteps.userSkipsOnboarding() - } - - @Test - fun create_second_profile_and_delete_first_one() { - mainScreenSteps.createNewProfile("Herbert Hepatitis B") - profileSettingSteps.userHasNumberOfProfiles(2) - profileSettingSteps.userDeletesProfile("Profil 1") - profileSettingSteps.userHasProfilesWithName(1, "Herbert Hepatitis B") - } - - @Test - fun delete_last_profile() { - mainScreenSteps.userTapsSettingsMenuButton() - profileSettingSteps.userDeletesProfile("Profil 1") - profileSettingSteps.createProfileAfterLastOneWasDeleted() - profileSettingSteps.userHasProfilesWithName(1, "Profil 1") - } - - @Test - fun abort_delete_last_profile() { - profileSettingSteps.userInterruptsDeletingProfile("Profil 1") - profileSettingSteps.userHasProfilesWithName(1, "Profil 1") - } - - @Test - fun abort_create_new_profile() { - mainScreenSteps.userTapsAddProfileButton() - mainScreenSteps.userCantConfirmCreation() - mainScreenSteps.userTapsAbort() - } - - @Test - fun create_profile_without_name() { - mainScreenSteps.userTapsAddProfileButton() - mainScreenSteps.userCantConfirmCreation() - mainScreenSteps.userTapsAbort() - profileSettingSteps.userHasNumberOfProfiles(1) - } - - @Test - fun delete_name_of_a_profile() { - profileSettingSteps.editProfileName("Profil 1", "") - profileSettingSteps.assertErrorMessageEmptyProfileName("Das Namensfeld darf nicht leer sein.") - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt deleted file mode 100644 index e8177cce..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.SmallTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps -import org.junit.Rule -import org.junit.Test - -@SmallTest -class OnboardingV1(fontScale: String) : WithFontScale(fontScale) { - - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingSteps by lazy { OnboardingSteps(composeRule) } - private fun restartApp() { - composeRule.activityRule.scenario.recreate() - } - - @Test - fun first_time() { - // Onboarding wird beim 1. App-Start angezeigt - onboardingSteps.userSeesWelcomeScreen() - } - - @Test - fun shown_again_if_not_finished() { - // Onboarding wird nach App-Neustart weiterhin angezeigt, solange es noch nicht beendet wurde - - onboardingSteps.userSeesWelcomeScreen() - restartApp() - onboardingSteps.userSeesWelcomeScreen() - } - - @Test - fun navigate_through_onboarding_without_analytics() { - // welcome screen - onboardingSteps.userSeesWelcomeScreen() - onboardingSteps.userIsFinishingTheOnboardingWithoutAnalytics() - } - - @Test - fun navigate_through_analytics_optional() { - // welcome screen - onboardingSteps.userSeesWelcomeScreen() - onboardingSteps.userIsFinishingTheOnboardingWithAnalytics() - } - - @Test - fun navigate_through_and_restart_app() { - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.MainScreen) - - restartApp() - - onboardingSteps.userIsNotSeeingTheOnboarding() - onboardingSteps.userSeesMainScreen() - } - - @Test - fun weak_password_blocks_next() { - // Onboarding Screen 3 >>> Zu schwaches Passwort = Weiter-Button inaktiv + Fehlermeldung - - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.Credentials) - // credentials screen - onboardingSteps.userSeesCredentialScreen() - - onboardingSteps.userEntersAWeakPasswordTwice() - - onboardingSteps.userDoesNotSeeContinueButton() - onboardingSteps.userSeesErrorMessageForPasswordStrength() - } - - @Test - fun no_password_blocks_next() { - // Onboarding Screen 3 >>> kein Passwort = Weiter-Button inaktiv + Fehlermeldung - - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.Credentials) - // credentials screen - onboardingSteps.userSeesCredentialScreen() - - onboardingSteps.userSwitchesToPasswordMode() - - onboardingSteps.userDoesNotSeeContinueButton() - onboardingSteps.userSeesErrorMessageForPasswordStrength() - } - - @Test - fun not_confirming_data_protection_blocks_next() { - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) - onboardingSteps.dataTermsSwitchDeactivated() - onboardingSteps.confirmContinueButtonIsDeactivated() - onboardingSteps.toggleDataTermsSwitch() - onboardingSteps.userSeesActivatedContinueButton() - onboardingSteps.toggleDataTermsSwitch() - onboardingSteps.confirmContinueButtonIsDeactivated() - } - - @Test - fun data_protection_can_be_read() { - // Datenschutzbestimmungen werden angezeigt und können geschlossen werden - - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) - - onboardingSteps.userDoesntSeeDataProtection() - onboardingSteps.userOpensDataProtection() - onboardingSteps.userSeesDataProtection() - onboardingSteps.userClosesDataProtection() - onboardingSteps.userDoesntSeeDataProtection() - } - - @Test - fun terms_of_use_can_be_read() { - // Nutzungsbedingungen werden angezeigt und können geschlossen werden - - onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) - - onboardingSteps.userSeesNoTermsOfUse() - onboardingSteps.userOpensTermsOfUse() - onboardingSteps.userSeesTermsOfUse() - onboardingSteps.userClosesTermsOfUse() - onboardingSteps.userSeesNoTermsOfUse() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt deleted file mode 100644 index 414e6440..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.SmallTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen -import org.junit.Rule -import org.junit.Test - -@SmallTest -class OnboardingV2(fontScale: String) : WithFontScale(fontScale) { - - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - - private fun restartApp() { - composeRule.activityRule.scenario.recreate() - } - - @Test - fun first_time() { - // Onboarding wird beim 1. App-Start angezeigt - - onboardingScreen.checkWelcomePageIsPresent() - } - - @Test - fun shown_again_if_not_finished() { - // Onboarding wird nach App-Neustart weiterhin angezeigt, solange es noch nicht beendet wurde - - onboardingScreen.checkWelcomePageIsPresent() - restartApp() - onboardingScreen.checkWelcomePageIsPresent() - } - - @Test - fun navigate_through() { - // welcome screen - onboardingScreen.checkWelcomePageIsPresent() - onboardingScreen.waitForSecondOnboardingPage() - - // data terms screen - onboardingScreen.checkDataTermsPageIsPresent() - - onboardingScreen.checkDataTermsSwitchDeactivated() - onboardingScreen.checkContinueTutorialButtonIsDeactivated() - - onboardingScreen.tapDataTermsSwitch() - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.tapContinueButton() - - // credentials screen - onboardingScreen.checkCredentialsPageIsPresent() - - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.StrongPassword) - onboardingScreen.enterPasswordB(TestConfig.StrongPassword) - onboardingScreen.tapContinueButton() - - // analytics screen - onboardingScreen.checkAnalyticsPageIsPresent() - - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.checkAnalyticsSwitchIsDeactivated() - - onboardingScreen.toggleAnalyticsSwitch() - onboardingScreen.waitForAnalyticsPage() - onboardingScreen.tapAcceptAnalyticsButton() - - onboardingScreen.checkAnalyticsSwitchIsActivated() - - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.tapContinueButton() - - // main screen - mainScreen.userSeesMainScreen() - } - - @Test - fun navigate_through_analytics_optional() { - // welcome screen - onboardingScreen.checkWelcomePageIsPresent() - onboardingScreen.waitForSecondOnboardingPage() - - // data terms screen - onboardingScreen.checkDataTermsPageIsPresent() - - onboardingScreen.checkDataTermsSwitchDeactivated() - onboardingScreen.checkContinueTutorialButtonIsDeactivated() - - onboardingScreen.tapDataTermsSwitch() - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.tapContinueButton() - - // credentials screen - onboardingScreen.checkCredentialsPageIsPresent() - - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.StrongPassword) - onboardingScreen.enterPasswordB(TestConfig.StrongPassword) - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.tapContinueButton() - - // analytics screen - onboardingScreen.checkAnalyticsPageIsPresent() - - onboardingScreen.checkContinueTutorialButtonIsEnabled() - onboardingScreen.checkAnalyticsSwitchIsDeactivated() - - onboardingScreen.tapContinueButton() - - // main screen - mainScreen.userSeesMainScreen() - } - - private enum class Page { - DataTerms, Credentials, Analytics, MainScreen - } - - @Suppress("ReturnCount") - private fun walkThroughOnboardingWithoutChecks(until: Page? = null) { - // welcome screen - onboardingScreen.checkWelcomePageIsPresent() - onboardingScreen.waitForSecondOnboardingPage() - - if (until == Page.DataTerms) return - - // data terms screen - onboardingScreen.checkDataTermsPageIsPresent() - - onboardingScreen.tapDataTermsSwitch() - - onboardingScreen.tapContinueButton() - - if (until == Page.Credentials) return - - // credentials screen - onboardingScreen.checkCredentialsPageIsPresent() - - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.StrongPassword) - onboardingScreen.enterPasswordB(TestConfig.StrongPassword) - onboardingScreen.checkContinueTutorialButtonIsEnabled() - - onboardingScreen.tapContinueButton() - - if (until == Page.Analytics) return - - // analytics screen - onboardingScreen.checkAnalyticsPageIsPresent() - - onboardingScreen.tapContinueButton() - - if (until == Page.MainScreen) return - - // main screen - mainScreen.userSeesMainScreen() - } - - @Test - fun navigate_through_and_restart_app() { - walkThroughOnboardingWithoutChecks() - - restartApp() - - mainScreen.userSeesMainScreen() - } - - @Test - fun weak_password_blocks_next() { - // Onboarding Screen 3 >>> Zu schwaches Passwort = Weiter-Button inaktiv + Fehlermeldung - - walkThroughOnboardingWithoutChecks(until = Page.Credentials) - - // credentials screen - onboardingScreen.checkCredentialsPageIsPresent() - - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.WeakPassword) - onboardingScreen.enterPasswordB(TestConfig.WeakPassword) - - onboardingScreen.checkContinueTutorialButtonIsDisabled() - onboardingScreen.checkPasswordErrorMessagePresent() - } - - @Test - fun data_protection_can_be_read() { - // Datenschutzbestimmungen werden angezeigt und können geschlossen werden - - walkThroughOnboardingWithoutChecks(until = Page.DataTerms) - - onboardingScreen.checkDataTermsPageIsPresent() - onboardingScreen.checkDataProtectionAreNotDisplayed() - onboardingScreen.openDataProtection() - onboardingScreen.checkDataProtectionIsDisplayed() - onboardingScreen.closeDataProtection() - onboardingScreen.checkDataProtectionAreNotDisplayed() - } - - @Test - fun terms_of_use_can_be_read() { - // Nutzungsbedingungen werden angezeigt und können geschlossen werden - - walkThroughOnboardingWithoutChecks(until = Page.DataTerms) - - onboardingScreen.checkDataTermsPageIsPresent() - onboardingScreen.checkTermsOfUseAreNotDisplayed() - onboardingScreen.openTermsOfUse() - onboardingScreen.checkTermsOfUseAreDisplayed() - onboardingScreen.closeTermsOfUse() - onboardingScreen.checkTermsOfUseAreNotDisplayed() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt deleted file mode 100644 index f96e2fc1..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.espresso.intent.rule.IntentsRule -import androidx.test.filters.LargeTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.core.TaskCollection -import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox -import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils -import de.gematik.ti.erp.app.test.test.core.prescription.SupplyOptionsType -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen -import de.gematik.ti.erp.app.test.test.screens.PharmacySearchScreen -import de.gematik.ti.erp.app.test.test.screens.PrescriptionOrderScreen -import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen -import org.junit.Rule -import org.junit.Test - -@LargeTest -class Orders(fontScale: String) : WithFontScale(fontScale) { - @get:Rule(order = 1) - val composeRule = createAndroidComposeRule() - - @get:Rule(order = 0) - val intentsRule = IntentsRule() - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } - private val pharmacySearchScreen by lazy { PharmacySearchScreen(composeRule) } - private val prescriptionOrderScreen by lazy { PrescriptionOrderScreen(composeRule) } - private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } - - @Test - fun order_prescription() { - val tasks = TaskCollection.generate( - 1, - TestConfig.AppDefaultVirtualEgkKvnr, - composeRule.activity.testWrapper - ) - - fun firstTask() = tasks.taskData.first() - - try { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - prescriptionsScreen.awaitPrescriptions() - - prescriptionsScreen.userSeesPZNPrescription( - data = firstTask().prescription, - inState = PrescriptionsScreen.PrescriptionState.Redeemable - ) - - mainScreen.userClicksBottomBarPharmacy() - - pharmacySearchScreen.userSeesPharmacyOverviewScreen() - - pharmacySearchScreen.userClicksSearchButton() - pharmacySearchScreen.userSeesPharmacySearchResultScreen() - pharmacySearchScreen.userSearchesForTestPharmacy() - pharmacySearchScreen.awaitSearchResults() - pharmacySearchScreen.userClicksOnTestPharmacy() - - pharmacySearchScreen.userSeesPharmacyOrderOptions() - pharmacySearchScreen.awaitOrderOptionsEnabled() - pharmacySearchScreen.userClicksOnOrderByPickUp() - - pharmacySearchScreen.userSeesPharmacyOrderSummaryScreen() - - // all contact information should be available; therefore the order button is enabled - pharmacySearchScreen.userSeesSendOrderButtonEnabled() - - pharmacySearchScreen.userClicksPrescriptionSelection() - pharmacySearchScreen.userSeesPrescriptionSelectionScreen() - pharmacySearchScreen.userDeselectsAllPrescriptions() - pharmacySearchScreen.userSelectsPrescription(firstTask().prescription) - pharmacySearchScreen.userClicksBack() - pharmacySearchScreen.userSeesPharmacyOrderSummaryScreen() - - pharmacySearchScreen.userSeesSendOrderButtonEnabled() - pharmacySearchScreen.userClicksSendOrderButton() - - mainScreen.userSeesMainScreen(10_000L) - prescriptionsScreen.refreshPrescriptions() - - prescriptionsScreen.userSeesPZNPrescription( - data = firstTask().prescription, - inState = PrescriptionsScreen.PrescriptionState.WaitForResponse - ) - - // pharmacy accepts task - tasks.accept(firstTask()) - val message = CommunicationPayloadInbox( - supplyOptionsType = SupplyOptionsType.Delivery, - url = "https://www.whatever.de/blablub", - infoText = "Hey u!", - pickUpCodeHR = "a1234567890", - pickUpCodeDMC = "b1234567890" - ) - tasks.reply(firstTask(), message) - - prescriptionsScreen.refreshPrescriptions() - - prescriptionsScreen.userSeesPZNPrescription( - data = firstTask().prescription, - inState = PrescriptionsScreen.PrescriptionState.InProgress - ) - - mainScreen.userClicksBottomBarOrders() - - prescriptionOrderScreen.userSeesOrderScreen() - prescriptionOrderScreen.awaitOrders() - - prescriptionOrderScreen.userClicksNewestOrder() - prescriptionOrderScreen.userSeesOrderDetailsScreen() - prescriptionOrderScreen.userSeesOnePrescription(firstTask().prescription) - prescriptionOrderScreen.userSeesAndClicksMessage() - prescriptionOrderScreen.userExpectsMessageContent(message) - prescriptionOrderScreen.userClicksMessageLink(message) - - prescriptionOrderScreen.userClosesMessageSheetBySwipe() - - prescriptionOrderScreen.userClicksPrescription(firstTask().prescription) - prescriptionsScreen.userSeesPrescriptionDetails() - prescriptionsScreen.userClicksClose() - prescriptionOrderScreen.userClicksBack() - - // dispense medication - tasks.dispense(firstTask()) - - mainScreen.userClicksBottomBarPrescriptions() - - prescriptionsScreen.awaitPrescriptionScreen() - prescriptionsScreen.refreshPrescriptions() - - prescriptionsScreen.userMissesPrescription(firstTask().prescription) - prescriptionsScreen.userClicksArchiveButton() - prescriptionsScreen.awaitArchivedPrescriptions() - prescriptionsScreen.userSeesPrescriptionInArchive( - data = firstTask().prescription, - inState = PrescriptionsScreen.PrescriptionState.Redeemed - ) - } finally { - tasks.deleteAll() - } - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt deleted file mode 100644 index d4d51f79..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.sharedtest.testresources.actions.PharmacyScreenAction -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class PharmacyUITest { - @get:Rule - val composeRule = createAndroidComposeRule() - - private val actions = PharmacyScreenAction(composeRule) - - @Test - fun pickupServiceSuccessTest() { - actions.pickupServiceSuccessTest() - } - - @Test - fun courierDeliverySuccessTest() { - actions.courierDeliverySuccessTest() - } - - @Test - fun pickupServiceMailDeliverySuccessTest() { - actions.pickupServiceMailDeliverySuccessTest() - } - - @Test - fun mailDeliverySuccessTest() { - actions.mailDeliverySuccessTest() - } - - @Test - fun pickupServiceCourierSuccessTest() { - actions.pickupServiceCourierSuccessTest() - } - - @Test - fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { - actions.pickupServiceMailDeliveryCourierDeliverySuccessTest() - } - - @Test - fun mailDeliveryCourierDeliverySuccessTest() { - actions.mailDeliveryCourierDeliverySuccessTest() - } - - @Test - fun pickupServiceFailTest() { - actions.pickupServiceFailTest() - } - - @Test - fun courierDeliveryFailTest() { - actions.courierDeliveryFailTest() - } - - @Test - fun mailDeliveryFailTest() { - actions.mailDeliveryFailTest() - } - - @Test - fun pickupServiceMailDeliveryCourierDeliveryFailTest() { - actions.pickupServiceMailDeliveryCourierDeliveryFailTest() - } - - @Test - fun pickupServiceMailDeliveryFailTest() { - actions.pickupServiceMailDeliveryFailTest() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetailsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetailsScreen.kt deleted file mode 100644 index 3612b9b5..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetailsScreen.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.LargeTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.core.TaskCollection -import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen -import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen -import org.junit.Rule -import org.junit.Test - -@LargeTest -class PrescriptionDetailsScreen(fontScale: String) : WithFontScale(fontScale) { - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } - private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } - - @Test - fun pzn_medication_details() { - val tasks = TaskCollection.generate(1, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) - val prescriptionData = tasks.taskData.first().prescription - - try { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - prescriptionsScreen.awaitPrescriptions() - - // details main page - prescriptionsScreen.clickOnPrescription(prescriptionData) - prescriptionsScreen.userSeesPrescriptionDetails() - prescriptionsScreen.userExpectsPrescriptionData(prescriptionData) - - // technical details - prescriptionsScreen.userClicksOnTechnicalDetails() - prescriptionsScreen.userSeesTechnicalDetailsScreen() - prescriptionsScreen.userExpectsTechnicalInformationData(prescriptionData) - - prescriptionsScreen.userClicksBack() - prescriptionsScreen.userSeesPrescriptionDetails() - - // patient page - prescriptionsScreen.userClicksOnPatientDetails() - prescriptionsScreen.userSeesPatientDetailsScreen() - prescriptionsScreen.userExpectsPatientDetailsData(prescriptionData) - - prescriptionsScreen.userClicksBack() - prescriptionsScreen.userSeesPrescriptionDetails() - - // organization page - prescriptionsScreen.userClicksOnOrganizationDetails() - prescriptionsScreen.userSeesOrganizationDetailsScreen() - prescriptionsScreen.userExpectsOrganizationDetailsData(prescriptionData) - - prescriptionsScreen.userClicksBack() - prescriptionsScreen.userSeesPrescriptionDetails() - - // medication page - prescriptionsScreen.userClicksOnMedicationDetails() - prescriptionsScreen.userSeesMedicationDetailsScreen() - prescriptionsScreen.userExpectsMedicationDetailsData(prescriptionData) - - prescriptionsScreen.userClicksBack() - prescriptionsScreen.userSeesPrescriptionDetails() - } finally { - tasks.deleteAll() - } - } - - @Test - fun delete_task() { - val tasks = TaskCollection.generate(1, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) - val prescriptionData = tasks.taskData.first().prescription - - try { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - prescriptionsScreen.awaitPrescriptions() - - // details main page - prescriptionsScreen.clickOnPrescription(prescriptionData) - prescriptionsScreen.userSeesPrescriptionDetails() - - prescriptionsScreen.userClicksMoreButton() - prescriptionsScreen.userClicksDeleteButton() - prescriptionsScreen.userSeesConfirmDeleteDialog() - prescriptionsScreen.userConfirmsDeletion() - - mainScreen.userSeesMainScreen() - prescriptionsScreen.awaitPrescriptions() - - prescriptionsScreen.userMissesPrescription(prescriptionData) - } finally { - tasks.deleteAll() - } - } - - @Test - fun main_screen_with_many_prescriptions() { - val tasks = TaskCollection.generate(6, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) - - try { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - - tasks.taskData.forEach { data -> - prescriptionsScreen.awaitPrescriptions() - prescriptionsScreen.clickOnPrescription(data.prescription) - prescriptionsScreen.userSeesPrescriptionDetails() - - // technical details - prescriptionsScreen.userClicksOnTechnicalDetails() - prescriptionsScreen.userSeesTechnicalDetailsScreen() - prescriptionsScreen.userExpectsTechnicalInformationData(data.prescription) - - prescriptionsScreen.userClicksBack() - prescriptionsScreen.userSeesPrescriptionDetails() - prescriptionsScreen.userClicksClose() - mainScreen.userSeesMainScreen() - } - - // TODO use expiresOn and TODO sorting is unstable (uses expiresOn with day accuracy) in app - // val fromOldToNew = tasks.prescriptions.sortedBy { it.authoredOn } - // prescriptionsScreen.userSeesPrescriptionSortedBy(fromOldToNew) - } finally { - tasks.deleteAll() - } - } - - @Test - fun main_screen_check_if_prescriptions_exist() { - val tasks = TaskCollection.generate(6, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) - - try { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.tapTooltips() - prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() - prescriptionsScreen.awaitPrescriptions() - - tasks.taskData.forEach { data -> - prescriptionsScreen.userSeesPZNPrescription(data.prescription) - } - } finally { - tasks.deleteAll() - } - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileScreenSettings.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileScreenSettings.kt deleted file mode 100644 index 4f00f7ad..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileScreenSettings.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.scenarios - -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.filters.MediumTest -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.test.test.WithFontScale -import de.gematik.ti.erp.app.test.test.core.sleep -import de.gematik.ti.erp.app.test.test.steps.CardWallScreenSteps -import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps -import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps -import de.gematik.ti.erp.app.test.test.steps.ProfileSettingsSteps -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@MediumTest -class ProfileScreenSettings(fontScale: String) : WithFontScale(fontScale) { - - @get:Rule - val composeRule = createAndroidComposeRule() - - private val onboardingSteps by lazy { OnboardingSteps(composeRule) } - private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } - private val profileSettingsSteps by lazy { ProfileSettingsSteps(composeRule) } - private val cardWallScreenSteps by lazy { CardWallScreenSteps(composeRule) } - - @Before - fun skipOnboarding() { - onboardingSteps.userSkipsOnboarding() - } - - @Test - fun no_token_in_profile_if_not_logged_in() { - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.checkNoTokenPresent() - profileSettingsSteps.checkTokenHintPresent() - } - - @Test - fun token_in_profile_if_logged_in() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.checkTokenPresent() - profileSettingsSteps.hintTextNotPresent() - } - - @Test - fun no_token_in_profile_after_logged_out() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - profileSettingsSteps.logoutViaProfileSettings() - profileSettingsSteps.noTokenPresentInProfileSettings() - profileSettingsSteps.checkTokenHintPresent( - "Sie erhalten einen Token, wenn Sie am Rezeptdienst angemeldet sind." - ) - } - - @Test - fun login_in_profile_settings_opens_cardwall() { - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.tapLoginButton() - profileSettingsSteps.userSeesCardwallWelcomeScreen() - } - - @Test - fun no_kvnr_in_profile_settings_if_never_logged_in() { - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.checkNoKVNRIsVisible() - } - - @Test - fun kvnr_in_profile_settings_is_visible_if_logged_in() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.checkKVNRIsVisible() - } - - @Test - fun kvnr_in_profile_settings_is_visible_if_logged_out() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - profileSettingsSteps.logoutViaProfileSettings() - profileSettingsSteps.openProfileSettings() - profileSettingsSteps.checkKVNRIsVisible() - } - - @Test - fun if_profile_changed_show_no_kvnr() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - mainScreenSteps.createNewProfile("Karoline Karies") - profileSettingsSteps.openProfileSettingsForCertainProfile(2) - profileSettingsSteps.checkNoKVNRIsVisible() - } - - @Test - fun show_no_access_protocol_screen() { - profileSettingsSteps.userSeesAuditEventsScreen(1) - profileSettingsSteps.userSeesEmptyStateForCertainProfile() - } - - @Test - fun show_no_access_protocol_screen_when_switching_to_logged_out_profile() { - cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() - mainScreenSteps.createNewProfile("Karoline Karies") - profileSettingsSteps.userSeesAuditEventsScreen(2) - profileSettingsSteps.userSeesEmptyStateForCertainProfile() - } - - @Test - fun show_protocol_after_logout() { - cardWallScreenSteps.setVirtualEGKWithPrescriptions() - composeRule.sleep(5_000L) // await audit events - profileSettingsSteps.userLogsOutOffProfile(1) - profileSettingsSteps.userSeesAuditEventsScreen(1) - profileSettingsSteps.userDoesNotSeesEmptyStateForCertainProfile() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt deleted file mode 100644 index 931d8605..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onNodeWithTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class AuditEventsScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesAuditEventsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.AuditEvents.AuditEventsScreen) - } - - fun checkAuditEventsDoNotExist() { - onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventHeader) - .assertIsDisplayed() - onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventInfo) - .assertIsDisplayed() - } - - fun checkAuditEventsExist() { - onAllNodesWithTag(TestTag.Profile.AuditEvents.AuditEvent)[0] - .assertIsDisplayed() - } - - fun checkNoAuditEventsHeaderAndInfoDoesNotExist() { - onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventHeader) - .assertDoesNotExist() - onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventInfo) - .assertDoesNotExist() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt deleted file mode 100644 index a65906b8..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class CardWallScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesIntroScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.Intro.IntroScreen) - } - fun userSeesPinScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.PIN.PinScreen) - } - fun userSeesCANScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.CAN.CANScreen) - } - - // ****** LoginScreen ****** - fun continueWithEGK() { - onNodeWithTag(TestTag.CardWall.ContinueButton) - .assertIsDisplayed() - .performClick() - } - - // ****** CAN and PIN Screen ****** - fun enterCAN() { - // CAN - onNodeWithTag(TestTag.CardWall.CAN.CANField) - .assertIsDisplayed() - .performTextInput(TestConfig.DefaultEGKCAN) - onNodeWithTag(TestTag.CardWall.ContinueButton) - .assertIsDisplayed() - .performClick() - } - - fun enterPin() { - // PIN - onNodeWithTag(TestTag.CardWall.PIN.PINField) - .assertIsDisplayed() - .performTextInput(TestConfig.DefaultEGKPassword) - onNodeWithTag(TestTag.CardWall.ContinueButton) - .assertIsDisplayed() - .performClick() - } - - fun saveCredentials() { - onNodeWithTag(TestTag.CardWall.StoreCredentials.Save) - .assertIsDisplayed() - .performClick() - onNodeWithTag(TestTag.CardWall.SecurityAcceptance.AcceptButton) - .assertIsDisplayed() - .performClick() - } - - fun dontSaveCredentials() { - onNodeWithTag(TestTag.CardWall.StoreCredentials.DontSave) - .assertIsDisplayed() - .performClick() - onNodeWithTag(TestTag.CardWall.ContinueButton) - .assertIsDisplayed() - .performClick() - } - - fun userSeesNfcScreen() { - onNodeWithTag(TestTag.CardWall.Nfc.NfcScreen) - .assertIsDisplayed() - } - - fun tapOrderEgkFromIntroScreen() { - onNodeWithTag(TestTag.CardWall.Intro.OrderEgkButton) - .assertIsDisplayed() - .performClick() - } - - fun tapOrderEgkFromCANScreen() { - onNodeWithTag(TestTag.CardWall.CAN.OrderEgkButton) - .assertIsDisplayed() - .performClick() - } - - fun tapOrderEgkFromPinScreen() { - onNodeWithTag(TestTag.CardWall.PIN.OrderEgkButton) - .assertIsDisplayed() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt deleted file mode 100644 index 5f16b191..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.assertIsToggleable -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTextReplacement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.VirtualEgk -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class DebugMenuScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesDebugMenuScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.DebugMenu.DebugMenuScreen) - } - - fun waitTillVirtualHealthCardIsSet() { - composeRule.awaitDisplay(10000) { - onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) - .assertIsNotEnabled() - } - composeRule.awaitDisplay(10000) { - onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) - .assertIsEnabled() - } - } - - fun tapSetVirtualCard() { - onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) - .performScrollToNode(hasTestTag(TestTag.DebugMenu.SetVirtualHealthCardButton)) - onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - waitTillVirtualHealthCardIsSet() - } - - fun closeDebugMenu() { - onNodeWithTag(TestTag.TopNavigation.CloseButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun fillCustomCertificateAndPrivateKey(virtualEgk: VirtualEgk) { - fillCertificateFieldWith(virtualEgk.certificate) - fillPrivateKeyFieldWith(virtualEgk.privateKey) - } - - fun fillCertificateFieldWith(certificateString: String) { - onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) - .performScrollToNode(hasTestTag(TestTag.DebugMenu.CertificateField)) - onNodeWithTag(TestTag.DebugMenu.CertificateField) - .assertIsDisplayed() - .performTextReplacement(certificateString) - } - - fun fillPrivateKeyFieldWith(privateKeyString: String) { - onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) - .performScrollToNode(hasTestTag(TestTag.DebugMenu.PrivateKeyField)) - onNodeWithTag(TestTag.DebugMenu.PrivateKeyField) - .assertIsDisplayed() - .performTextReplacement(privateKeyString) - } - - fun tapFakeNFCCapabilitiesSwitch() { - onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) - .performScrollToNode(hasTestTag(TestTag.DebugMenu.FakeNFCCapabilities)) - - onNodeWithTag(TestTag.DebugMenu.FakeNFCCapabilities) - .assertIsDisplayed() - .assertIsToggleable() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt deleted file mode 100644 index 84fb7a80..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeDown -import androidx.compose.ui.test.swipeUp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class MainScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesMainScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreen) - } - - fun userSeesBottomSheet(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreenBottomSheet.Modal) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.Modal).performTouchInput { swipeUp() } - } - - fun checkProfileHasState(profileName: String, profileState: String) { - onNodeWithText(profileName, substring = true) - .assertIsDisplayed() - onNodeWithText(profileState, substring = true) - .assertIsDisplayed() - } - - fun refreshMainScreenBySwipe() { - onNodeWithTag(TestTag.Main.MainScreen) - .assertIsDisplayed() - .performTouchInput { swipeDown() } - } - - fun tapLoginButton() { - onNodeWithTag(TestTag.Main.LoginButton) - .assertIsDisplayed() - .performClick() - } - - fun tapConnectLater() { - userSeesBottomSheet(5000) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ConnectLaterButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapTooltips() { - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - } - - fun tapSettingsButton() { - onNodeWithTag(TestTag.BottomNavigation.SettingsButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksBottomBarPrescriptions() { - onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksBottomBarPharmacy() { - onNodeWithTag(TestTag.BottomNavigation.PharmaciesButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksPharmacySearchBar() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - TestConfig.ScreenChangeTimeout - } - - fun userClicksBottomBarOrders() { - onNodeWithTag(TestTag.BottomNavigation.OrdersButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapAddProfileButton() { - onNodeWithTag(TestTag.Main.AddProfileButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun enterProfileName(name: String) { - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) - .assertIsDisplayed() - .performClick() - .performTextInput(name) - } - - fun tapNewProfileConfirmButton() { - userSeesBottomSheet(5000L) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapCancelAddProfileButton() { - userSeesBottomSheet() - onNodeWithTag(TestTag.Main.LoginButton) // BottomSheet has no CancelButton - .performClick() - } - - fun assertConfirmationCanNotBeClicked() { - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) - .assertIsNotEnabled() - } - - /** - * cancel user login - * click-though all tool-tips - * click on pharmacies bottom button - */ - fun openPharmaciesFromBottomBarFromStart() { - tapConnectLater() - tapTooltips() - userClicksBottomBarPharmacy() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt deleted file mode 100644 index f9136dfb..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.assertIsOff -import androidx.compose.ui.test.assertIsOn -import androidx.compose.ui.test.assertIsToggleable -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeLeft -import androidx.compose.ui.test.swipeRight -import androidx.compose.ui.test.swipeUp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class OnboardingScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun checkTutorialIsNotPresent() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertDoesNotExist() - } - - fun waitForSecondOnboardingPage() { - composeRule.awaitDisplay(5000L, TestTag.Onboarding.DataTermsScreen) - } - - fun waitForAnalyticsPage() { - composeRule.awaitDisplay(5000L, TestTag.Onboarding.Analytics.ScreenContent) - } - - fun tapContinueButton() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .performClick() - } - - fun switchToPasswordMode() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordTab) - .assertIsDisplayed() - .performClick() - } - - fun enterPasswordA(password: String) { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldA) - .assertIsDisplayed() - .performClick() - .assertIsFocused() - .performTextInput(password) - } - - fun enterPasswordB(password: String) { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldB) - .assertIsDisplayed() - .performClick() - .assertIsFocused() - .performTextInput(password) - } - - fun tapDataTermsSwitch() { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .performClick() - } - - fun closeDataProtection() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun checkDataProtectionIsDisplayed() { - onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) - .assertIsDisplayed() - } - - fun openDataProtection() { - onNodeWithTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton) - .assertIsDisplayed() - .performClick() - } - - fun checkDataProtectionAreNotDisplayed() { - onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) - .assertDoesNotExist() - } - - fun closeTermsOfUse() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun openTermsOfUse() { - onNodeWithTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton) - .assertIsDisplayed() - .performClick() - } - - fun checkTermsOfUseAreDisplayed() { - onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) - .assertIsDisplayed() - } - - fun checkTermsOfUseAreNotDisplayed() { - onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) - .assertDoesNotExist() - } - - fun checkNoPasswordErrorMessagePresent() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) - .assertIsDisplayed() - .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "sufficient")) - } - - fun checkPasswordErrorMessagePresent() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) - .assertIsDisplayed() - .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "insufficient")) - } - - fun checkContinueTutorialButtonIsEnabled() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsEnabled() - } - - fun checkContinueTutorialButtonIsDeactivated() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsNotEnabled() - } - - fun checkDataTermsSwitchDeactivated() { - onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOff() - } - - fun checkWelcomePageIsPresent() { - onNodeWithTag(TestTag.Onboarding.WelcomeScreen) - .assertExists() - } - - fun checkCredentialsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.CredentialsScreen) - .assertExists() - } - - fun checkAnalyticsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.AnalyticsScreen) - .assertExists() - } - - fun checkDataTermsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.DataTermsScreen) - .assertExists() - } - - fun swipeToNextTutorialStep() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertIsDisplayed() - .performTouchInput { swipeLeft() } - } - - fun swipeToPreviousTutorialStep() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertIsDisplayed() - .performTouchInput { swipeRight() } - } - - fun checkContinueTutorialButtonIsDisabled() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsNotEnabled() - } - - fun tapSkipOnboardingButton() { - onNodeWithTag(TestTag.Onboarding.SkipOnboardingButton) - .assertIsDisplayed() - .performClick() - } - - fun checkAnalyticsSwitchIsDeactivated() { - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .performScrollTo() - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOff() - } - - fun checkAnalyticsSwitchIsActivated() { - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .performScrollTo() - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOn() - } - - fun toggleAnalyticsSwitch() { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .performClick() - } - - fun tapAcceptAnalyticsButton() { - onNodeWithTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt deleted file mode 100644 index 93d2429c..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class OrderEgkScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesOrderEgkScreenScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.OrderEgk.OrderEgkScreen) - } - - fun tapNFCExplanationPageLink() { - onNodeWithTag(TestTag.Settings.OrderEgk.NFCExplanationPageLink) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun checkIfAtLeastFourInsurerIsVisible() { - for (i in 0..3) { - onAllNodesWithTag(TestTag.Settings.InsuranceCompanyList.ListOfInsuranceButtons)[i] - .assertIsDisplayed() - .assertHasClickAction() - } - } - - fun chooseInsurance(insurance: String) { - onNodeWithText(insurance, substring = true) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun checkOrderPossibilities(orderPossibility: String) { - when (orderPossibility) { - "Keine Bestellmöglichkeit" -> { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton) - .assertDoesNotExist() - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderPinButton) - .assertDoesNotExist() - } - "Karten & PIN, Nur PIN" -> { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderPinButton) - .assertIsDisplayed() - .assertHasClickAction() - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - } - } - - fun checkContactPossibilities(contactPossibility: String) { - if (contactPossibility.contains("Telefon")) { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton) - .assertIsDisplayed() - .assertHasClickAction() - } - if (contactPossibility.contains("Webseite")) { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton) - .assertIsDisplayed() - .assertHasClickAction() - } - if (contactPossibility.contains("Mail")) { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.MailToButton) - .assertIsDisplayed() - .assertHasClickAction() - } - if (contactPossibility.contains("Keine Kontaktmöglichkeit")) { - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton) - .assertDoesNotExist() - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton) - .assertDoesNotExist() - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.MailToButton) - .assertDoesNotExist() - onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.NoContactInfoTextBox) - .assertIsDisplayed() - } - } - - fun tapOrderCardAbort() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt deleted file mode 100644 index eeb4c67c..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotSelected -import androidx.compose.ui.test.assertIsSelected -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasAnyDescendant -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isSelected -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performImeAction -import androidx.compose.ui.test.performScrollToKey -import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTextInput -import de.gematik.ti.erp.app.PrescriptionIds -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.TestConfig.WaitTimeout1Sec -import de.gematik.ti.erp.app.test.test.core.awaitDisplay -import de.gematik.ti.erp.app.test.test.core.hasPharmacyId -import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId -import de.gematik.ti.erp.app.test.test.core.prescription.Prescription -import de.gematik.ti.erp.app.test.test.core.sleep - -class PharmacySearchScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesPharmacyOverviewScreen() { - onNodeWithTag(TestTag.PharmacySearch.OverviewScreen, useUnmergedTree = true) - .assertIsDisplayed() - } - - fun userClicksSearchButton() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) - .performClick() - } - - fun userSeesPharmacySearchResultScreen() { - onNodeWithTag(TestTag.PharmacySearch.ResultScreen) - .assertIsDisplayed() - } - - fun userSearchesForTestPharmacy() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchField) - .performClick() - .assertIsFocused() - .performTextInput(TestConfig.PharmacyName) - - onNodeWithTag(TestTag.PharmacySearch.TextSearchField) - .performImeAction() - } - - fun awaitSearchResults() { - composeRule.awaitDisplay(20_000L) { - onNodeWithTag(TestTag.PharmacySearch.ResultContent) - .assertIsDisplayed() - .assert(hasAnyChild(hasTestTag(TestTag.PharmacySearch.PharmacyListEntry))) - } - composeRule.sleep(1_000L) - } - - fun userClicksOnTestPharmacy() { - onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) - .assertIsDisplayed() - .performScrollToNode(hasPharmacyId(TestConfig.PharmacyTelematikId)) - .onChildren() - .filterToOne(hasPharmacyId(TestConfig.PharmacyTelematikId)) - .performClick() - } - - fun userClicksOnPharmacyFromListByName(name: String) { - composeRule.sleep(WaitTimeout1Sec) - onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) - .performScrollToNode(hasAnyDescendant(hasText(name))) - .onChildren() - .filterToOne(hasAnyDescendant(hasText(name))) - .performClick() - } - - fun userSeesPharmacyOrderOptions() { - composeRule.sleep(WaitTimeout1Sec) - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.Content, useUnmergedTree = true) - .assertExists() - } - - fun awaitOrderOptionsEnabled() { - composeRule.sleep(1_000L) - } - - fun dismissOrderOptionsBottomSheet() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchField) - .assertIsDisplayed() - .performClick() - } - - fun userClicksOnOrderByCourierDelivery() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun userClicksOnOrderByPickUp() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun checkToastMessageWhenOrderOptionClicked() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.ComposeToast, useUnmergedTree = true) - .assertExists() - } - - fun checkAndClickNoPrescriptionDialog() { - onNodeWithTag(TestTag.AlertDialog.ConfirmButton).assertIsDisplayed().performClick() - } - - fun userClicksOnOrderByMailDelivery() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun userSeesPharmacyOrderSummaryScreen() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.Screen) - .assertIsDisplayed() - } - - fun userSeesSendOrderButtonEnabled() { - // asynchronous process enabling the button - composeRule.awaitDisplay(1_000L) { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) - .assertIsDisplayed() - .assertIsEnabled() - } - } - - fun userClicksPrescriptionSelection() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton) - .assertIsDisplayed() - .performClick() - } - - fun userSeesPrescriptionSelectionScreen() { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen) - .assertIsDisplayed() - } - - fun userDeselectsAllPrescriptions() { - val prescriptionIds = onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .fetchSemanticsNode() - .config[PrescriptionIds]!! - - prescriptionIds.forEach { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .performScrollToKey("prescription-$it") - .onChildren() - .filterToOne(hasPrescriptionId(it).and(isSelected())) - .performClick() - } - } - - fun userSelectsPrescription(prescription: Prescription) { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .assertIsDisplayed() - .performScrollToKey("prescription-${prescription.taskId}") - .onChildren() - .filterToOne(hasPrescriptionId(prescription.taskId)) - .assertIsNotSelected() - .assertIsDisplayed() - .performClick() - .assertIsSelected() - } - - fun userClicksBack() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun userClicksSendOrderButton() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun openOrderOptionsByPharmacyName(name: String) { - userSeesPharmacyOverviewScreen() - userClicksSearchButton() - awaitSearchResults() - userClicksOnPharmacyFromListByName(name) - userSeesPharmacyOrderOptions() - awaitOrderOptionsEnabled() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt deleted file mode 100644 index 808f016b..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import android.app.Activity -import android.app.Instrumentation -import android.content.Intent -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertTextContains -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.onFirst -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToKey -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeDown -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.matcher.IntentMatchers -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.core.await -import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId -import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox -import de.gematik.ti.erp.app.test.test.core.prescription.Prescription -import de.gematik.ti.erp.app.test.test.core.sleep - -class PrescriptionOrderScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesOrderScreen() { - onNodeWithTag(TestTag.Orders.Content) - .assertIsDisplayed() - } - - fun awaitOrders() { - composeRule.await(20_000L) { - onNodeWithTag(TestTag.Orders.Content) - .assertIsDisplayed() - .assert(hasAnyChild(hasTestTag(TestTag.Orders.OrderListItem))) - } - composeRule.sleep(2500L) - } - - fun userClicksNewestOrder() { - onNodeWithTag(TestTag.Orders.Content) - .assertIsDisplayed() - .onChildren() - .filter(hasTestTag(TestTag.Orders.OrderListItem)) - .onFirst() - .assertIsDisplayed() - .performClick() - } - - fun userSeesOrderDetailsScreen() { - onNodeWithTag(TestTag.Orders.Details.Screen) - .assertIsDisplayed() - } - - fun userSeesOnePrescription(prescription: Prescription) { - onNodeWithTag(TestTag.Orders.Details.Content) - .assertIsDisplayed() - .performScrollToKey("prescriptions") - .onChildren() - .filterToOne(hasPrescriptionId(prescription.taskId)) - .assertIsDisplayed() - } - - fun userSeesAndClicksMessage() { - onNodeWithTag(TestTag.Orders.Details.Content) - .assertIsDisplayed() - .performScrollToKey("prescriptions") - .onChildren() - .filterToOne(hasTestTag(TestTag.Orders.Details.MessageListItem)) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userExpectsMessageContent(message: CommunicationPayloadInbox) { - onNodeWithTag(TestTag.Orders.Messages.Content) - .assertIsDisplayed() - - message.infoText?.let { - onNodeWithTag(TestTag.Orders.Messages.Text) - .assertIsDisplayed() - .assertTextContains(message.infoText) - } - ?: onNodeWithTag(TestTag.Orders.Messages.Text).assertDoesNotExist() - - message.url?.let { - onNodeWithTag(TestTag.Orders.Messages.Link) - .assertIsDisplayed() - - onNodeWithTag(TestTag.Orders.Messages.LinkButton) - .assertIsDisplayed() - .assertIsEnabled() - .assertHasClickAction() - } - ?: onNodeWithTag(TestTag.Orders.Messages.Link).assertDoesNotExist() - - if (message.pickUpCodeHR != null || message.pickUpCodeDMC != null) { - onNodeWithTag(TestTag.Orders.Messages.Code) - .assertIsDisplayed() - - if (message.pickUpCodeDMC != null) { - onNodeWithTag(TestTag.Orders.Messages.CodeLabelContent) - .assertTextContains(message.pickUpCodeDMC, substring = true) - } else if (message.pickUpCodeHR != null) { - onNodeWithTag(TestTag.Orders.Messages.CodeLabelContent) - .assertTextContains(message.pickUpCodeHR, substring = true) - } - } else { - onNodeWithTag(TestTag.Orders.Messages.Code).assertDoesNotExist() - } - } - - fun userClicksMessageLink(message: CommunicationPayloadInbox) { - Intents.intending(IntentMatchers.hasData(message.url)) - .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())) - - onNodeWithTag(TestTag.Orders.Messages.LinkButton) - .assertIsDisplayed() - .assertIsEnabled() - .assertHasClickAction() - .performClick() - - Intents.intended(IntentMatchers.hasData(message.url)) - } - - fun userClosesMessageSheetBySwipe() { - onNodeWithTag(TestTag.Orders.Details.Content) - .performTouchInput { - swipeDown() - } - } - - fun userClicksPrescription(prescription: Prescription) { - onNodeWithTag(TestTag.Orders.Details.Content) - .assertIsDisplayed() - .performScrollToKey("prescriptions") - .onChildren() - .filterToOne(hasPrescriptionId(prescription.taskId)) - .assertIsDisplayed() - .performClick() - } - - fun userClicksBack() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt deleted file mode 100644 index a45304bf..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import android.util.Log -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertTextContains -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToIndex -import androidx.compose.ui.test.performScrollToKey -import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeDown -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.assertHasText -import de.gematik.ti.erp.app.test.test.core.await -import de.gematik.ti.erp.app.test.test.core.hasInsuranceState -import de.gematik.ti.erp.app.test.test.core.hasMedicationCategory -import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId -import de.gematik.ti.erp.app.test.test.core.hasSubstitutionAllowed -import de.gematik.ti.erp.app.test.test.core.hasSupplyForm -import de.gematik.ti.erp.app.test.test.core.prescription.Prescription -import de.gematik.ti.erp.app.test.test.core.sleep -import org.junit.Assert.assertTrue - -@Suppress("UnusedPrivateMember") -class PrescriptionsScreen(private val composeRule: ComposeTestRule) : SemanticsNodeInteractionsProvider by composeRule { - enum class PrescriptionState { - Redeemable, // ready - WaitForResponse, // artificial state - InProgress, // inProgress - Redeemed - } - - fun userClicksBack() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksClose() { - onNodeWithTag(TestTag.TopNavigation.CloseButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun awaitPrescriptions() { - composeRule.await(TestConfig.LoadPrescriptionsTimeout) { - onNodeWithTag(TestTag.Prescriptions.Content) - .assertIsDisplayed() - .assert(hasAnyChild(hasTestTag(TestTag.Prescriptions.FullDetailPrescription))) - } - composeRule.sleep(2500L) - } - - fun awaitArchivedPrescriptions() { - composeRule.await(TestConfig.LoadPrescriptionsTimeout) { - onNodeWithTag(TestTag.Prescriptions.Archive.Content) - .assertIsDisplayed() - .assert(hasAnyChild(hasTestTag(TestTag.Prescriptions.FullDetailPrescription))) - } - composeRule.sleep(2500L) - } - - fun awaitPrescriptionScreen() { - composeRule.await(TestConfig.ScreenChangeTimeout) { - onNodeWithTag(TestTag.Prescriptions.Content) - .assertIsDisplayed() - } - } - - fun refreshPrescriptions() { - onNodeWithTag(TestTag.Prescriptions.Content) - .performScrollToIndex(0) - .performTouchInput { - swipeDown() - } - composeRule.sleep(2500L) - } - - fun userSeesPZNPrescription( - data: Prescription, - inState: PrescriptionState = PrescriptionState.Redeemable - ) { - val prescriptionNode = onNodeWithTag(TestTag.Prescriptions.Content, useUnmergedTree = true) - .assertIsDisplayed() - .performScrollToKey("prescription-${data.taskId}") - .onChildren() - .filterToOne(hasPrescriptionId(data.taskId)) - .onChildren() - - prescriptionNode - .filterToOne(hasTestTag(TestTag.Prescriptions.FullDetailPrescriptionName)) - .assertTextContains(data.medication?.name ?: "") - - val expectedTestTag = when (inState) { - PrescriptionState.Redeemable -> TestTag.Prescriptions.PrescriptionRedeemable - PrescriptionState.WaitForResponse -> TestTag.Prescriptions.PrescriptionWaitForResponse - PrescriptionState.InProgress -> TestTag.Prescriptions.PrescriptionInProgress - PrescriptionState.Redeemed -> TestTag.Prescriptions.PrescriptionRedeemed - } - prescriptionNode - .filterToOne(hasTestTag(expectedTestTag)) - .assertIsDisplayed() - } - - fun userSeesPrescriptionInArchive( - data: Prescription, - inState: PrescriptionState = PrescriptionState.Redeemable - ) { - val prescriptionNode = onNodeWithTag(TestTag.Prescriptions.Archive.Content, useUnmergedTree = true) - .assertIsDisplayed() - .performScrollToKey("prescription-${data.taskId}") - .onChildren() - .filterToOne(hasPrescriptionId(data.taskId)) - .onChildren() - - prescriptionNode - .filterToOne(hasTestTag(TestTag.Prescriptions.FullDetailPrescriptionName)) - .assertTextContains(data.medication?.name ?: "") - - val expectedTestTag = when (inState) { - PrescriptionState.Redeemable -> TestTag.Prescriptions.PrescriptionRedeemable - PrescriptionState.WaitForResponse -> TestTag.Prescriptions.PrescriptionWaitForResponse - PrescriptionState.InProgress -> TestTag.Prescriptions.PrescriptionInProgress - PrescriptionState.Redeemed -> TestTag.Prescriptions.PrescriptionRedeemed - } - prescriptionNode - .filterToOne(hasTestTag(expectedTestTag)) - .assertIsDisplayed() - } - - fun clickOnPrescription(data: Prescription) { - onNodeWithTag(TestTag.Prescriptions.Content) - .assertIsDisplayed() - .performScrollToKey("prescription-${data.taskId}") - .onChildren() - .filterToOne(hasPrescriptionId(data.taskId)) - .performClick() - } - - fun userSeesPrescriptionDetails() { - composeRule.await(TestConfig.ScreenChangeTimeout) { - onNodeWithTag(TestTag.Prescriptions.Details.Screen) - .assertIsDisplayed() - } - } - - fun userClicksMoreButton() { - onNodeWithTag(TestTag.Prescriptions.Details.MoreButton) - .assertIsDisplayed() - .performClick() - } - - fun userClicksDeleteButton() { - onNodeWithTag(TestTag.Prescriptions.Details.DeleteButton) - .assertIsDisplayed() - .performClick() - } - - fun userSeesConfirmDeleteDialog() { - onNodeWithTag(TestTag.AlertDialog.Modal, useUnmergedTree = true) - .assertIsDisplayed() - } - - fun userConfirmsDeletion() { - onNodeWithTag(TestTag.AlertDialog.ConfirmButton) - .assertIsDisplayed() - .performClick() - } - - fun userMissesPrescription(data: Prescription) { - onNodeWithTag(TestTag.Prescriptions.Content) - .assertIsDisplayed() - .onChildren() - .filter(hasPrescriptionId(data.taskId)) - .assertCountEquals(0) - } - - fun userClicksArchiveButton() { - onNodeWithTag(TestTag.Prescriptions.Content) - .performScrollToNode(hasTestTag(TestTag.Prescriptions.ArchiveButton)) - - onNodeWithTag(TestTag.Prescriptions.ArchiveButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userSeesPrescriptionSortedBy(prescriptions: List) { - val node = onNodeWithTag(TestTag.Prescriptions.Content) - .fetchSemanticsNode() - val config = node.config[SemanticsProperties.IndexForKey] - - val all = prescriptions.map { prescription -> - val index = config.invoke("prescription-${prescription.taskId}") - "$index - ${prescription.taskId} - ${prescription.authoredOn}" - }.joinToString("\n") - - prescriptions.fold(-1) { previousIndex, prescription -> - val index = config.invoke("prescription-${prescription.taskId}") - val msg = "Index should match: $index > $previousIndex for ${prescription.taskId}\n$all" - assertTrue(msg, index > previousIndex) - index - } - } - - // - - private fun onDetailsNode(testTag: String, contentTestTag: String) = - onNodeWithTag(contentTestTag) - .assertIsDisplayed() - .performScrollToNode(hasTestTag(testTag)) - .onChildren() - .filterToOne(hasTestTag(testTag)) - - private fun assertWith( - testTag: String, - contentTestTag: String, - with: SemanticsNodeInteraction.() -> Unit - ) { - onDetailsNode(testTag, contentTestTag).with() - } - - private fun assertText(testTag: String, contentTestTag: String, vararg data: String?) { - val node = onDetailsNode(testTag, contentTestTag).assertHasText(includeEditableText = false) - val dataFiltered = data.filterNotNull() - Log.d("assertText", "Assert hasText: ${dataFiltered.joinToString(" and ") { "[$it]" }} on $testTag") - - dataFiltered.forEach { - node - .assertTextContains(it, substring = true, ignoreCase = true) - } - } - - private fun assertDetailsText(testTag: String, vararg data: String?) = - assertText(testTag = testTag, contentTestTag = TestTag.Prescriptions.Details.Content, *data) - - fun userExpectsPrescriptionData(data: Prescription) { - assertDetailsText(TestTag.Prescriptions.Details.MedicationButton, data.medication?.name) - assertDetailsText(TestTag.Prescriptions.Details.PatientButton, data.patient?.firstName, data.patient?.lastName) - assertDetailsText(TestTag.Prescriptions.Details.PrescriberButton, data.practitioner?.name) - } - - // technical details - - fun userClicksOnTechnicalDetails() { - onDetailsNode(TestTag.Prescriptions.Details.TechnicalInformationButton, TestTag.Prescriptions.Details.Content) - .assertHasClickAction() - .performClick() - } - - fun userSeesTechnicalDetailsScreen() { - onNodeWithTag(TestTag.Prescriptions.Details.TechnicalInformation.Screen) - .assertIsDisplayed() - } - - private fun assertTechnicalText(testTag: String, vararg data: String?) = - assertText( - testTag = testTag, - contentTestTag = TestTag.Prescriptions.Details.TechnicalInformation.Content, - *data - ) - - fun userExpectsTechnicalInformationData(data: Prescription) { - assertTechnicalText(TestTag.Prescriptions.Details.TechnicalInformation.TaskId, data.taskId) - assertTechnicalText(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode, data.accessCode) - } - - // patient - - fun userClicksOnPatientDetails() { - onDetailsNode(TestTag.Prescriptions.Details.PatientButton, TestTag.Prescriptions.Details.Content) - .assertHasClickAction() - .performClick() - } - - fun userSeesPatientDetailsScreen() { - onNodeWithTag(TestTag.Prescriptions.Details.Patient.Screen) - .assertIsDisplayed() - } - - private fun assertPatientText(testTag: String, vararg data: String?) = - assertText( - testTag = testTag, - contentTestTag = TestTag.Prescriptions.Details.Patient.Content, - *data - ) - - fun userExpectsPatientDetailsData(data: Prescription) { - assertPatientText(TestTag.Prescriptions.Details.Patient.KVNR, data.patient?.kvnr) - assertPatientText(TestTag.Prescriptions.Details.Patient.BirthDate, data.patient?.birthDate) - assertPatientText(TestTag.Prescriptions.Details.Patient.Name, data.patient?.firstName, data.patient?.lastName) - assertPatientText(TestTag.Prescriptions.Details.Patient.InsuranceName, data.coverage?.insuranceName) - assertPatientText( - TestTag.Prescriptions.Details.Patient.Address, - data.patient?.city, - data.patient?.postal, - data.patient?.street - ) - assertWith( - testTag = TestTag.Prescriptions.Details.Patient.InsuranceState, - contentTestTag = TestTag.Prescriptions.Details.Patient.Content, - with = { - data.coverage?.insuranceState?.let { - assert(hasInsuranceState(it)) - } - } - ) - } - - // organization - - fun userClicksOnOrganizationDetails() { - onDetailsNode(TestTag.Prescriptions.Details.OrganizationButton, TestTag.Prescriptions.Details.Content) - .assertHasClickAction() - .performClick() - } - - fun userSeesOrganizationDetailsScreen() { - onNodeWithTag(TestTag.Prescriptions.Details.Organization.Screen) - .assertIsDisplayed() - } - - private fun assertOrganizationText(testTag: String, vararg data: String?) = - assertText( - testTag = testTag, - contentTestTag = TestTag.Prescriptions.Details.Organization.Content, - *data - ) - - fun userExpectsOrganizationDetailsData(data: Prescription) { - assertOrganizationText(TestTag.Prescriptions.Details.Organization.Name, data.practitioner?.officeName) - assertOrganizationText( - TestTag.Prescriptions.Details.Organization.Address, - data.practitioner?.city, - data.practitioner?.postal, - data.practitioner?.street - ) - assertOrganizationText(TestTag.Prescriptions.Details.Organization.BSNR, data.practitioner?.bsnr) - assertOrganizationText(TestTag.Prescriptions.Details.Organization.Phone, data.practitioner?.phone) - assertOrganizationText(TestTag.Prescriptions.Details.Organization.EMail, data.practitioner?.email) - } - - // medication - - fun userClicksOnMedicationDetails() { - onDetailsNode(TestTag.Prescriptions.Details.MedicationButton, TestTag.Prescriptions.Details.Content) - .assertHasClickAction() - .performClick() - } - - fun userSeesMedicationDetailsScreen() { - onNodeWithTag(TestTag.Prescriptions.Details.Medication.Screen) - .assertIsDisplayed() - } - - private fun assertMedicationText(testTag: String, vararg data: String?) = - assertText( - testTag = testTag, - contentTestTag = TestTag.Prescriptions.Details.Medication.Content, - *data - ) - - fun userExpectsMedicationDetailsData(data: Prescription) { - assertMedicationText(TestTag.Prescriptions.Details.Medication.Name, data.medication?.name) - assertMedicationText(TestTag.Prescriptions.Details.Medication.Amount, data.medication?.amount.toString()) - assertMedicationText(TestTag.Prescriptions.Details.Medication.PZN, data.medication?.pzn.toString()) - assertMedicationText(TestTag.Prescriptions.Details.Medication.DosageInstruction, data.medication?.dosage) - assertWith( - testTag = TestTag.Prescriptions.Details.Medication.SubstitutionAllowed, - contentTestTag = TestTag.Prescriptions.Details.Medication.Content, - with = { - data.medication?.substitutionAllowed?.let { - assert(hasSubstitutionAllowed(it)) - } - } - ) - assertWith( - testTag = TestTag.Prescriptions.Details.Medication.SupplyForm, - contentTestTag = TestTag.Prescriptions.Details.Medication.Content, - with = { - data.medication?.supplyForm?.let { - assert(hasSupplyForm(it)) - } - } - ) - assertWith( - testTag = TestTag.Prescriptions.Details.Medication.Category, - contentTestTag = TestTag.Prescriptions.Details.Medication.Content, - with = { - data.medication?.category?.let { - assert(hasMedicationCategory(it)) - } - } - ) - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt deleted file mode 100644 index 84eea35d..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTextReplacement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class ProfileSettingsScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesProfileSettingsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.ProfileScreen) - } - fun userSeesDeleteProfileAlert(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.AlertDialog.Modal) - } - - fun tapDisplayTokensButton() { - onNodeWithTag(TestTag.Profile.ProfileScreenContent) - .performScrollToNode(hasTestTag(TestTag.Profile.OpenTokensScreenButton)) - onNodeWithTag(TestTag.Profile.OpenTokensScreenButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun checkKVNRNotVisible() { - onNodeWithTag(TestTag.Profile.InsuranceId) - .assertDoesNotExist() - } - - fun checkKVNRIsVisible() { - onNodeWithTag(TestTag.Profile.InsuranceId) - .assertIsDisplayed() - .assert(!hasText("")) - } - - fun tapLoginButton() { - onNodeWithTag(TestTag.Profile.LoginButton) - .assertIsDisplayed() - .performClick() - } - - fun assertHintTextNotPresent() { - onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) - .assertDoesNotExist() - } - - fun tapThreeDotsMenuButton() { - onNodeWithTag(TestTag.Profile.ThreeDotMenuButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapAuditEventsButton() { - onNodeWithTag(TestTag.Profile.ProfileScreenContent) - .performScrollToNode(hasTestTag(TestTag.Profile.OpenAuditEventsScreenButton)) - onNodeWithTag(TestTag.Profile.OpenAuditEventsScreenButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapLogoutButton() { - onNodeWithTag(TestTag.Profile.LogoutButton) - .assertIsDisplayed() - .performClick() - } - - fun closeProfileSettings() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun tapDeleteProfile() { - onNodeWithTag(TestTag.Profile.DeleteProfileButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapConfirmDeleteProfile() { - onNodeWithTag(TestTag.AlertDialog.ConfirmButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapAbortDeleteButton() { - onNodeWithTag(TestTag.AlertDialog.CancelButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapEditProfileNameButton() { - onNodeWithTag(TestTag.Profile.EditProfileNameButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun enterNewProfileName(newProfileName: String) { - onNodeWithTag(TestTag.Profile.NewProfileNameField) - .assertIsDisplayed() - .performClick() - .performTextReplacement(newProfileName) - } - - fun assertErrorMessageEmptyProfileName(errorMessage: String) { - onNodeWithText(errorMessage) - .assertIsDisplayed() - } - - fun tapEditProfileIconButton() { - onNodeWithTag(TestTag.Profile.EditProfileImageButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun changeProfilePictureColor(color: String) { - val colorToTestTag = mapOf( - "grau" to TestTag.Profile.EditProfileIcon.ColorSelectorSpringGrayButton, - "gelb" to TestTag.Profile.EditProfileIcon.ColorSelectorSunDewButton, - "rosa" to TestTag.Profile.EditProfileIcon.ColorSelectorPinkButton, - "grün" to TestTag.Profile.EditProfileIcon.ColorSelectorTreeButton, - "blau" to TestTag.Profile.EditProfileIcon.ColorSelectorBlueMoonButton - ) - - val testTag = colorToTestTag[color.lowercase()] - - testTag?.let { - onNodeWithTag(testTag) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - } - - fun tapBackToSettingsButton() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt deleted file mode 100644 index 29303154..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertTextContains -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.assertHasText -import de.gematik.ti.erp.app.test.test.core.awaitDisplay - -class ProfileTokenListScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesProfileTokenListScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.TokenList.TokenScreen) - } - - fun checkTokensDoNotExist() { - onNodeWithTag(TestTag.Profile.TokenList.AccessToken) - .assertDoesNotExist() - onNodeWithTag(TestTag.Profile.TokenList.SSOToken) - .assertDoesNotExist() - } - - fun checkAccessTokenPresent() { - onNodeWithTag(TestTag.Profile.TokenList.AccessToken) - .assertIsDisplayed() - .assertHasText() - } - - fun checkSSOTokenPresent() { - onNodeWithTag(TestTag.Profile.TokenList.SSOToken) - .assertIsDisplayed() - .assertHasText() - } - - fun assertHeaderTextNotEmpty() { - onNodeWithTag(TestTag.Profile.TokenList.NoTokenHeader) - .assertIsDisplayed() - .assertHasText() - } - - fun assertInfoTextNotEmpty() { - onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) - .assertIsDisplayed() - .assertHasText() - } - - fun assertInfoTextPresent(text: String) { - onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) - .assertIsDisplayed() - .assertTextContains(text, substring = true) - } - - fun closeTokenList() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt deleted file mode 100644 index 5dce5557..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay -import junit.framework.TestCase.assertTrue - -class SettingsScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesSettingsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.SettingsScreen) - } - fun userSeesCreateNewProfileAlert(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.AddProfileDialog.Modal) - } - - private val profileSettingsScreen by lazy { ProfileSettingsScreen(composeRule) } - - fun goToFirstProfileDetails() { - goToProfileDetails(0) - } - - fun goToProfileDetails(index: Int) { - tapProfileDetailsButton(index) - profileSettingsScreen.userSeesProfileSettingsScreen() - } - - fun assertAmountOfProfiles(numberOfProfiles: Int) { - assertTrue( - onAllNodesWithTag(TestTag.Settings.ProfileButton) - .fetchSemanticsNodes().size == numberOfProfiles - ) - } - - fun tapDebugMenuButton() { - onNodeWithTag(TestTag.Settings.DebugMenuButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun enterProfileName(name: String) { - onNodeWithTag(TestTag.Settings.AddProfileDialog.ProfileNameTextField) - .assertIsDisplayed() - .performClick() - .performTextInput(name) - } - - fun tapNewProfileConfirmButton() { - onNodeWithTag(TestTag.Settings.AddProfileDialog.ConfirmButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapReturnToPrescriptionScreen() { - onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapOrderEgk() { - onNodeWithTag(TestTag.Settings.OrderNewCardButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapProfileWithName(profileName: String) { - onNodeWithText(profileName) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapProfileDetailsButton(profileNumber: Int) { - onAllNodesWithTag(TestTag.Settings.ProfileButton)[profileNumber] - .assertIsDisplayed() - .performClick() - } - - fun checkAmountOfProfilesWithNames(numberOfProfiles: Int, profileNames: String) { - assert(onAllNodesWithText(profileNames).fetchSemanticsNodes().size == numberOfProfiles) - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt deleted file mode 100644 index 0c910023..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.steps - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.test.platform.app.InstrumentationRegistry -import de.gematik.ti.erp.app.test.test.VirtualEgk1 -import de.gematik.ti.erp.app.test.test.VirtualEgkWithPrescription -import de.gematik.ti.erp.app.test.test.screens.CardWallScreen -import de.gematik.ti.erp.app.test.test.screens.DebugMenuScreen -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.SettingsScreen - -class CardWallScreenSteps(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - private val mainScreen by lazy { MainScreen(composeRule) } - private val cardWallScreen by lazy { CardWallScreen(composeRule) } - private val settingsScreen by lazy { SettingsScreen(composeRule) } - private val debugMenuScreen by lazy { DebugMenuScreen(composeRule) } - - fun userStartsAndFinishsTheCardwallSuccessfully() { - InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc enable") - - mainScreen.userSeesMainScreen() - mainScreen.refreshMainScreenBySwipe() - mainScreen.tapLoginButton() - - cardWallScreen.continueWithEGK() - cardWallScreen.enterCAN() - cardWallScreen.enterPin() - cardWallScreen.dontSaveCredentials() - cardWallScreen.userSeesNfcScreen() - - InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc disable") - Thread.sleep(2000) - InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc enable") - - mainScreen.userSeesMainScreen(15000) - } - - fun userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() { - mainScreen.userSeesMainScreen() - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapDebugMenuButton() - debugMenuScreen.userSeesDebugMenuScreen() - debugMenuScreen.tapSetVirtualCard() - debugMenuScreen.closeDebugMenu() - settingsScreen.tapReturnToPrescriptionScreen() - mainScreen.userSeesMainScreen() - } - - fun fakeNFCCapabilities() { - mainScreen.userSeesMainScreen() - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapDebugMenuButton() - debugMenuScreen.userSeesDebugMenuScreen() - debugMenuScreen.tapFakeNFCCapabilitiesSwitch() - debugMenuScreen.closeDebugMenu() - settingsScreen.tapReturnToPrescriptionScreen() - mainScreen.userSeesMainScreen() - } - - fun setCustomEgk() { - // only for demonstration purposes - mainScreen.userSeesMainScreen() - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapDebugMenuButton() - debugMenuScreen.userSeesDebugMenuScreen() - debugMenuScreen.fillCustomCertificateAndPrivateKey(VirtualEgk1) - debugMenuScreen.tapSetVirtualCard() - debugMenuScreen.closeDebugMenu() - settingsScreen.tapReturnToPrescriptionScreen() - mainScreen.userSeesMainScreen() - } - - fun setVirtualEGKWithPrescriptions() { - // only for demonstration purposes - mainScreen.userSeesMainScreen() - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapDebugMenuButton() - debugMenuScreen.userSeesDebugMenuScreen() - debugMenuScreen.fillCustomCertificateAndPrivateKey(VirtualEgkWithPrescription) - debugMenuScreen.tapSetVirtualCard() - debugMenuScreen.closeDebugMenu() - settingsScreen.tapReturnToPrescriptionScreen() - mainScreen.userSeesMainScreen() - } - - fun userClicksOrderHealthCardFromCardWallIntroScreen() { - cardWallScreen.userSeesIntroScreen() - cardWallScreen.tapOrderEgkFromIntroScreen() - } - - fun userClicksOrderHealthCardFromCardWallCANScreen() { - cardWallScreen.userSeesIntroScreen() - cardWallScreen.continueWithEGK() - cardWallScreen.userSeesCANScreen() - cardWallScreen.tapOrderEgkFromCANScreen() - } - - fun userClicksOrderHealthCardFromCardWallPinScreen() { - cardWallScreen.userSeesIntroScreen() - cardWallScreen.continueWithEGK() - cardWallScreen.userSeesCANScreen() - cardWallScreen.enterCAN() - cardWallScreen.userSeesPinScreen() - cardWallScreen.tapOrderEgkFromPinScreen() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt deleted file mode 100644 index cad1658c..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.steps - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.junit4.ComposeTestRule -import de.gematik.ti.erp.app.test.test.screens.MainScreen - -class MainScreenSteps(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - private val mainScreen by lazy { MainScreen(composeRule) } - - fun userTapsSettingsMenuButton() { - mainScreen.tapSettingsButton() - } - - fun createNewProfile(profileName: String) { - userTapsAddProfileButton() - mainScreen.enterProfileName(profileName) - if ("" != profileName) { - mainScreen.tapNewProfileConfirmButton() - mainScreen.userSeesMainScreen() - } - } - - fun userTapsAddProfileButton() { - mainScreen.tapAddProfileButton() - mainScreen.userSeesBottomSheet() - } - - fun userTapsAbort() { - mainScreen.userSeesBottomSheet() - mainScreen.tapCancelAddProfileButton() - mainScreen.userClicksBottomBarPrescriptions() - } - - fun userCantConfirmCreation() { - mainScreen.userSeesBottomSheet() - mainScreen.assertConfirmationCanNotBeClicked() - } - - fun userTapsConnect() { - mainScreen.tapLoginButton() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt deleted file mode 100644 index fabb05a8..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.steps - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.junit4.ComposeTestRule -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.sleep -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen - -class OnboardingSteps( - private val composeRule: ComposeTestRule -) : SemanticsNodeInteractionsProvider by composeRule { - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - - enum class Page { - DataTerms, Credentials, Analytics, MainScreen - } - - fun userSeesMainScreen() { - mainScreen.userSeesMainScreen() - } - - fun userIsNotSeeingTheOnboarding() { - onboardingScreen.checkTutorialIsNotPresent() - } - - fun userIsFinishingTheOnboardingWithoutAnalytics() { - userNavigatesToOnboardingScreenName(Page.MainScreen) - } - - fun userIsFinishingTheOnboardingWithAnalytics() { - userNavigatesToOnboardingScreenName(Page.Analytics) - onboardingScreen.toggleAnalyticsSwitch() - onboardingScreen.waitForAnalyticsPage() - onboardingScreen.tapAcceptAnalyticsButton() - onboardingScreen.checkAnalyticsSwitchIsActivated() - onboardingScreen.checkContinueTutorialButtonIsEnabled() - onboardingScreen.tapContinueButton() - mainScreen.userSeesMainScreen() - } - - fun userSkipsOnboarding() { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.tapConnectLater() - mainScreen.userSeesMainScreen() - tapToGetRidOfTour() - } - - fun tapToGetRidOfTour() { - mainScreen.userClicksBottomBarPrescriptions() - mainScreen.userClicksBottomBarPrescriptions() - mainScreen.userClicksBottomBarPrescriptions() - mainScreen.userClicksBottomBarPrescriptions() - mainScreen.userClicksBottomBarPrescriptions() - composeRule.sleep(2000L) - } - - fun userSeesWelcomeScreen() { - onboardingScreen.checkWelcomePageIsPresent() - } - - fun userNavigatesToOnboardingScreenName(page: Page) { - when (page) { - // go to Data Terms Screen - Page.DataTerms -> { - onboardingScreen.waitForSecondOnboardingPage() - onboardingScreen.checkDataTermsPageIsPresent() - } - Page.Credentials -> { // go to Password Screen - onboardingScreen.waitForSecondOnboardingPage() - onboardingScreen.tapDataTermsSwitch() - onboardingScreen.tapContinueButton() - onboardingScreen.checkCredentialsPageIsPresent() - } - Page.Analytics -> { // go to Analytics Screen - userNavigatesToOnboardingScreenName(Page.Credentials) - onboardingScreen.switchToPasswordMode() - userEntersStrongEnoughPasswordTwice() - onboardingScreen.tapContinueButton() - userSeesAnalyticsScreen() - } - Page.MainScreen -> { - userNavigatesToOnboardingScreenName(Page.Analytics) - onboardingScreen.tapContinueButton() - mainScreen.userSeesMainScreen() - } - } - } - - private fun userSeesAnalyticsScreen() { - onboardingScreen.checkAnalyticsPageIsPresent() - onboardingScreen.checkContinueTutorialButtonIsEnabled() - onboardingScreen.checkAnalyticsSwitchIsDeactivated() - } - - fun userSeesCredentialScreen() { - onboardingScreen.checkCredentialsPageIsPresent() - } - - fun dataTermsSwitchDeactivated() { - onboardingScreen.checkDataTermsSwitchDeactivated() - } - - fun confirmContinueButtonIsDeactivated() { - onboardingScreen.checkContinueTutorialButtonIsDeactivated() - } - - fun toggleDataTermsSwitch() { - onboardingScreen.tapDataTermsSwitch() - } - - fun userEntersAWeakPasswordTwice() { - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.WeakPassword) - onboardingScreen.enterPasswordB(TestConfig.WeakPassword) - } - - fun userEntersStrongEnoughPasswordTwice() { - onboardingScreen.switchToPasswordMode() - onboardingScreen.enterPasswordA(TestConfig.StrongPassword) - onboardingScreen.enterPasswordB(TestConfig.StrongPassword) - onboardingScreen.checkNoPasswordErrorMessagePresent() - } - - fun userSwitchesToPasswordMode() { - onboardingScreen.switchToPasswordMode() - } - - fun userDoesNotSeeContinueButton() { - onboardingScreen.checkContinueTutorialButtonIsDisabled() - } - - fun userSeesActivatedContinueButton() { - onboardingScreen.checkContinueTutorialButtonIsEnabled() - } - - fun userSeesTermsOfUse() { - onboardingScreen.checkTermsOfUseAreDisplayed() - } - - fun userSeesNoTermsOfUse() { - onboardingScreen.checkTermsOfUseAreNotDisplayed() - } - - fun userOpensTermsOfUse() { - onboardingScreen.openTermsOfUse() - } - - fun userClosesTermsOfUse() { - onboardingScreen.closeTermsOfUse() - } - - fun userDoesntSeeDataProtection() { - onboardingScreen.checkDataProtectionAreNotDisplayed() - } - - fun userOpensDataProtection() { - onboardingScreen.openDataProtection() - } - - fun userSeesDataProtection() { - onboardingScreen.checkDataProtectionIsDisplayed() - } - - fun userClosesDataProtection() { - onboardingScreen.closeDataProtection() - } - - fun userSeesErrorMessageForPasswordStrength() { - onboardingScreen.checkPasswordErrorMessagePresent() - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt deleted file mode 100644 index da59de5f..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.steps - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.junit4.ComposeTestRule -import de.gematik.ti.erp.app.test.test.screens.AuditEventsScreen -import de.gematik.ti.erp.app.test.test.screens.CardWallScreen -import de.gematik.ti.erp.app.test.test.screens.MainScreen -import de.gematik.ti.erp.app.test.test.screens.ProfileSettingsScreen -import de.gematik.ti.erp.app.test.test.screens.ProfileTokenListScreen -import de.gematik.ti.erp.app.test.test.screens.SettingsScreen - -class ProfileSettingsSteps( - private val composeRule: ComposeTestRule -) : SemanticsNodeInteractionsProvider by composeRule { - - private val mainScreen by lazy { MainScreen(composeRule) } - private val profileSettingsScreen by lazy { ProfileSettingsScreen(composeRule) } - private val profileTokenListScreen by lazy { ProfileTokenListScreen(composeRule) } - private val cardWallScreen by lazy { CardWallScreen(composeRule) } - private val settingsScreen by lazy { SettingsScreen(composeRule) } - private val auditEventsScreen by lazy { AuditEventsScreen(composeRule) } - - fun openProfileSettings() { - mainScreen.tapSettingsButton() - settingsScreen.goToFirstProfileDetails() - profileSettingsScreen.userSeesProfileSettingsScreen() - } - - fun openProfileSettingsForCertainProfile(profileIndex: Int) { - mainScreen.userSeesMainScreen() - // to keep scenarios human-readable, we'll hide the fact, that the index actually starts at zero ¯\_(ツ)_/¯ - mainScreen.tapSettingsButton() - settingsScreen.goToProfileDetails(profileIndex - 1) - } - - fun checkNoTokenPresent() { - profileSettingsScreen.tapDisplayTokensButton() - profileTokenListScreen.userSeesProfileTokenListScreen() - profileTokenListScreen.checkTokensDoNotExist() - } - - fun checkTokenHintPresent() { - profileTokenListScreen.assertHeaderTextNotEmpty() - profileTokenListScreen.assertInfoTextNotEmpty() - } - - fun checkTokenHintPresent(text: String) { - profileSettingsScreen.tapDisplayTokensButton() - profileTokenListScreen.assertInfoTextPresent(text) - } - - fun checkNoKVNRIsVisible() { - profileSettingsScreen.checkKVNRNotVisible() - } - - fun checkKVNRIsVisible() { - profileSettingsScreen.checkKVNRIsVisible() - } - - fun tapLoginButton() { - profileSettingsScreen.tapThreeDotsMenuButton() - profileSettingsScreen.tapLoginButton() - } - - fun userSeesCardwallWelcomeScreen() { - cardWallScreen.userSeesIntroScreen() - } - - fun checkTokenPresent() { - profileSettingsScreen.tapDisplayTokensButton() - profileTokenListScreen.userSeesProfileTokenListScreen() - profileTokenListScreen.checkAccessTokenPresent() - profileTokenListScreen.checkSSOTokenPresent() - profileTokenListScreen.closeTokenList() - } - - fun hintTextNotPresent() { - profileSettingsScreen.tapDisplayTokensButton() - profileSettingsScreen.assertHintTextNotPresent() - } - - fun logoutViaProfileSettings() { - mainScreen.tapSettingsButton() - settingsScreen.goToFirstProfileDetails() - profileSettingsScreen.tapThreeDotsMenuButton() - profileSettingsScreen.tapLogoutButton() - profileSettingsScreen.closeProfileSettings() - } - - fun noTokenPresentInProfileSettings() { - mainScreen.tapSettingsButton() - settingsScreen.goToFirstProfileDetails() - profileSettingsScreen.tapDisplayTokensButton() - profileTokenListScreen.checkTokensDoNotExist() - profileTokenListScreen.closeTokenList() - } - - fun userSeesAuditEventsScreen(profileIndex: Int) { - mainScreen.userSeesMainScreen() - mainScreen.tapSettingsButton() - // -1 durch Differenzen zum normalen Sprachgebrauch - settingsScreen.goToProfileDetails(profileIndex - 1) - profileSettingsScreen.tapAuditEventsButton() - auditEventsScreen.userSeesAuditEventsScreen() - } - - fun userSeesEmptyStateForCertainProfile() { - auditEventsScreen.userSeesAuditEventsScreen() - auditEventsScreen.checkAuditEventsDoNotExist() - } - - fun userLogsOutOffProfile(index: Int) { - mainScreen.tapSettingsButton() - settingsScreen.goToProfileDetails(index - 1) - profileSettingsScreen.tapThreeDotsMenuButton() - profileSettingsScreen.tapLogoutButton() - profileSettingsScreen.closeProfileSettings() - mainScreen.userSeesMainScreen() - } - - fun userDoesNotSeesEmptyStateForCertainProfile() { - auditEventsScreen.userSeesAuditEventsScreen() - auditEventsScreen.checkNoAuditEventsHeaderAndInfoDoesNotExist() - } - - fun userHasNumberOfProfiles(numberOfProfiles: Int) { - mainScreen.tapSettingsButton() - settingsScreen.assertAmountOfProfiles(numberOfProfiles) - mainScreen.userClicksBottomBarPrescriptions() - mainScreen.userSeesMainScreen() - } - - fun userDeletesProfile(profileName: String) { - mainScreen.tapSettingsButton() - settingsScreen.tapProfileWithName(profileName) - profileSettingsScreen.userSeesProfileSettingsScreen() - profileSettingsScreen.tapThreeDotsMenuButton() - profileSettingsScreen.tapDeleteProfile() - profileSettingsScreen.tapConfirmDeleteProfile() - } - - fun userHasProfilesWithName(numberOfProfiles: Int, profileNames: String) { - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.checkAmountOfProfilesWithNames(numberOfProfiles, profileNames) - } - - fun createProfileAfterLastOneWasDeleted() { - settingsScreen.userSeesCreateNewProfileAlert() - settingsScreen.enterProfileName("Profil 1") - settingsScreen.tapNewProfileConfirmButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapReturnToPrescriptionScreen() - mainScreen.userSeesMainScreen() - } - - fun userInterruptsDeletingProfile(profileName: String) { - navigateToProfileWithName(profileName) - profileSettingsScreen.userSeesProfileSettingsScreen() - profileSettingsScreen.tapThreeDotsMenuButton() - profileSettingsScreen.tapDeleteProfile() - profileSettingsScreen.tapAbortDeleteButton() - profileSettingsScreen.tapBackToSettingsButton() - } - - fun editProfileName(profileName: String, newProfileName: String) { - navigateToProfileWithName(profileName) - profileSettingsScreen.tapEditProfileNameButton() - profileSettingsScreen.enterNewProfileName(newProfileName) - } - - fun assertErrorMessageEmptyProfileName(errorMessage: String) { - profileSettingsScreen.assertErrorMessageEmptyProfileName(errorMessage) - } - - fun userChangesProfilePictureOfProfileFromColorTo(profileName: String, color: String) { - navigateToProfileWithName(profileName) - profileSettingsScreen.tapEditProfileIconButton() - profileSettingsScreen.changeProfilePictureColor(color) - profileSettingsScreen.tapBackToSettingsButton() - } - - private fun navigateToProfileWithName(profileName: String) { - mainScreen.tapSettingsButton() - settingsScreen.userSeesSettingsScreen() - settingsScreen.tapProfileWithName(profileName) - } -} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt deleted file mode 100644 index a9c05ed4..00000000 --- a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.test.test.steps - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.junit4.ComposeTestRule -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.test.test.TestConfig -import de.gematik.ti.erp.app.test.test.core.awaitDisplay -import de.gematik.ti.erp.app.test.test.screens.OrderEgkScreen -import de.gematik.ti.erp.app.test.test.screens.SettingsScreen - -class SettingScreenSteps(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - private val settingsScreen by lazy { SettingsScreen(composeRule) } - private val orderEgkScreen by lazy { OrderEgkScreen(composeRule) } - - fun userWantsToOrderNewCard() { - settingsScreen.tapOrderEgk() - orderEgkScreen.userSeesOrderEgkScreenScreen() - } - - fun userUsesLinkAboutNFC() { - orderEgkScreen.tapNFCExplanationPageLink() - } - - fun userIsNotInERezeptAppAnymore() { - // TODO Wir sind im browser und wollen den URL prüfen. URL = "das-e-rezept-fuer-deutschland.de" - } - - fun userSeesAListOfInsurances() { - orderEgkScreen.userSeesOrderEgkScreenScreen() - orderEgkScreen.checkIfAtLeastFourInsurerIsVisible() - } - - fun userChoosesInsurance(insurance: String) { - orderEgkScreen.chooseInsurance(insurance) - } - - fun userSeesOrderOptionScreen() { - composeRule.awaitDisplay(TestConfig.ScreenChangeTimeout, TestTag.Settings.OrderEgk.SelectOrderOptionScreen) - } - - fun userSeesHealthCardOrderContactScreen() { - composeRule.awaitDisplay(TestConfig.ScreenChangeTimeout, TestTag.Settings.OrderEgk.HealthCardOrderContactScreen) - } - - fun userSeesPossibilitiesWhatCanBeOrdered(orderPossibility: String) { - orderEgkScreen.checkOrderPossibilities(orderPossibility) - } - - fun userSeesPossibilitiesHowCanBeOrdered(contactPossibility: String) { - orderEgkScreen.checkContactPossibilities(contactPossibility) - } - - fun userAbortsOrderingOfNewCard() { - orderEgkScreen.tapOrderCardAbort() - } - - fun userSeesSettingsScreen() { - settingsScreen.userSeesSettingsScreen() - } -} diff --git a/app/android/src/main/AndroidManifest.xml b/app/android/src/main/AndroidManifest.xml index e7d7cf8e..8280b0a1 100644 --- a/app/android/src/main/AndroidManifest.xml +++ b/app/android/src/main/AndroidManifest.xml @@ -28,9 +28,14 @@ android:name="android.hardware.camera" android:required="false" /> + + { object : DispatchProvider {} } + bindProvider { + val context = instance() + context.resources + } + bindProvider { + val context = instance() + context.assets + } + bindProvider { + val context = instance() + context.mainLooper + } bindSingleton(ApplicationPreferencesTag) { val context = instance() context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) @@ -57,6 +68,13 @@ val appModules = DI.Module("appModules") { bindSingleton(NetworkSecurePreferencesTag) { val context = instance() + @Requirement( + "O.Arch_2#1", + "O.Data_2#4", + "O.Data_3#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Data storage using EncryptedSharedPreferences." + ) EncryptedSharedPreferences.create( context, NETWORK_SECURE_PREFS_FILE_NAME, diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt index 9f25f6ee..1cbaf52c 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt b/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt index d90324aa..097968c2 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.info @@ -40,6 +40,8 @@ class DefaultBuildConfigInformation : BuildConfigInformation { defaultValue = NFC_NOT_AVAILABLE ) ?: NFC_NOT_AVAILABLE + override fun isMockedApp(): Boolean = false + companion object { private const val DARK_THEME_ON = "an" private const val DARK_THEME_OFF = "aus" diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt b/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt index 016c42b0..8374daa2 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.info.di diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt index ae1739d9..6ac3ccdf 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt index 3977bc00..75d04e93 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv diff --git a/app/android/src/main/res/xml/data_extraction_rules.xml b/app/android/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 00000000..78a61775 --- /dev/null +++ b/app/android/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/android/src/main/res/xml/network_security_config.xml b/app/android/src/main/res/xml/network_security_config.xml index 4517087e..b8f65835 100644 --- a/app/android/src/main/res/xml/network_security_config.xml +++ b/app/android/src/main/res/xml/network_security_config.xml @@ -1,67 +1,11 @@ - - erp-ref.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - - - erp-test.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - - - erp.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - - - - idp-ref.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - idp-test.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - idp.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - apovzd.app.ti-dienste.de - - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - + + + + diff --git a/app/android/src/test/java/android/util/Base64.kt b/app/android/src/test/java/android/util/Base64.kt deleted file mode 100644 index 79f2d834..00000000 --- a/app/android/src/test/java/android/util/Base64.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:JvmName("Base64") - -package android.util - -fun decode(input: ByteArray, flag: Int): ByteArray { - return java.util.Base64.getDecoder().decode(input) -} - -fun decode(input: String, flag: Int): ByteArray { - return when (flag) { - 0 -> java.util.Base64.getDecoder().decode(input) - 8 -> java.util.Base64.getUrlDecoder().decode(input) - else -> java.util.Base64.getUrlDecoder().decode(input) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt b/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt index aab2eae0..fe01f78e 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt index d934d68d..e9e0ba9d 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt deleted file mode 100644 index 3d1c5c35..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") - -package de.gematik.ti.erp.app.invoice.usecase - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.junit.Rule -import kotlin.test.Test -import kotlin.test.BeforeTest - -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class InvoiceUseCaseTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - - @MockK - lateinit var useCase: InvoiceUseCase - - @MockK - lateinit var repositpry: InvoiceRepository - - @BeforeTest - fun setup() { - MockKAnnotations.init(this) - useCase = spyk( - InvoiceUseCase(repositpry, coroutineRule.dispatchers) - ) - } - - @Test - fun `invoices - should return invoices sorted by timestamp and grouped by year`() = - - runTest { - every { useCase.invoicesFlow("1234") } returns flowOf(listOf(pkvInvoice, pkvInvoice2)) - - val invoices = useCase.invoices("1234").first() - - println("size : " + invoices.size) - assertEquals( - mapOf( - Pair( - first = later.toLocalDateTime(TimeZone.currentSystemDefault()).year, - second = listOf(pkvInvoice2) - ), - Pair( - first = now.toLocalDateTime(TimeZone.currentSystemDefault()).year, - second = listOf(pkvInvoice) - ) - ), - invoices - ) - } - - @Test - fun `invoice create dmc payload`() = - assertEquals("{\"urls\":[\"ChargeItem/01234?ac=98765\"]}", pkvInvoice.dmcPayload) -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt b/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt index 9e50f8c3..31c8f1e3 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.usecase -import de.gematik.ti.erp.app.utils.asFhirTemporal import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.asFhirTemporal import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.plus @@ -28,7 +28,7 @@ import kotlinx.datetime.plus val now = Clock.System.now() val later = now.plus(8760, DateTimeUnit.HOUR) -val pkvInvoice = InvoiceData.PKVInvoice( +val pkvInvoiceRecord = InvoiceData.PKVInvoiceRecord( profileId = "1234", taskId = "01234", accessCode = "98765", @@ -66,11 +66,12 @@ val pkvInvoice = InvoiceData.PKVInvoice( null, null, false, null, SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None ), - whenHandedOver = now.asFhirTemporal() + whenHandedOver = now.asFhirTemporal(), + consumed = false ) -val pkvInvoice2 = InvoiceData.PKVInvoice( +val pkvInvoiceRecord2 = InvoiceData.PKVInvoiceRecord( profileId = "23456", taskId = "65432", accessCode = "98765", @@ -108,6 +109,7 @@ val pkvInvoice2 = InvoiceData.PKVInvoice( null, null, false, null, SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None ), - whenHandedOver = later.asFhirTemporal() + whenHandedOver = later.asFhirTemporal(), + consumed = false ) diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/mocks/MessageMocks.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/mocks/MessageMocks.kt new file mode 100644 index 00000000..c9b88244 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/mocks/MessageMocks.kt @@ -0,0 +1,363 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.mocks + +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.messages.repository.CachedPharmacy +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.datetime.Instant + +object MessageMocks { + + internal const val MOCK_ORDER_ID = "testOrderId" + + internal const val MOCK_TASK_ID_01 = "123-001" + + internal const val MOCK_TASK_ID_02 = "456-002" + + private const val MOCK_COMMUNICATION_ID_01 = "CID-123-001" + + private const val MOCK_COMMUNICATION_ID_02 = "CID-456-002" + + internal const val MOCK_TRANSACTION_ID = "transactionMockId" + + internal val MOCK_PHARMACY_O1 = CachedPharmacy(name = "Pharmacy1", telematikId = "123") + + internal val MOCK_PHARMACY_O2 = CachedPharmacy(name = "Pharmacy2", telematikId = "456") + + private const val MOCK_PRACTITIONER_NAME = "Dr. John Doe" + + internal const val MOCK_PROFILE_IDENTIFIER = "testProfileIdentifier" + + private val MOCK_PAYLOAD = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "info_text":"mock message." , + "pickUpCodeHR":"T01__R01" , + "pickUpCodeDMC":"Test_01___Rezept_01___abcdefg12345" , + "url":"https://www.tree.fm/forest/33" + } + """.trimIndent() + + private val MOCK_PAYLOAD_02 = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "info_text":"mock message_02." , + "pickUpCodeHR":"T01__R02" , + "pickUpCodeDMC":"Test_01___Rezept_02___abcdefg12345" , + "url":"https://www.tree.fm/forest/35" + } + """.trimIndent() + + private val MOCK_PRACTITIONER = SyncedTaskData.Practitioner( + name = MOCK_PRACTITIONER_NAME, + qualification = "", + practitionerIdentifier = " " + ) + + private val MOCK_CHIP_INFO = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = null + ) + + private val MOCK_PRESCRIPTION_01 = Prescription.SyncedPrescription( + taskId = MOCK_TASK_ID_01, + name = null, + redeemedOn = null, + expiresOn = Instant.fromEpochSeconds(123456), + state = SyncedTaskData.SyncedTask.Expired( + expiredOn = Instant.fromEpochSeconds(123456) + ), + isIncomplete = false, + organization = MOCK_PRACTITIONER_NAME, + authoredOn = Instant.fromEpochSeconds(123456), + acceptUntil = Instant.fromEpochSeconds(123456), + isDirectAssignment = false, + prescriptionChipInformation = MOCK_CHIP_INFO + ) + + private val MOCK_PRESCRIPTION_02 = Prescription.SyncedPrescription( + taskId = MOCK_TASK_ID_02, + name = null, + redeemedOn = null, + expiresOn = Instant.fromEpochSeconds(123456), + state = SyncedTaskData.SyncedTask.Expired( + expiredOn = Instant.fromEpochSeconds(123456) + ), + isIncomplete = false, + organization = MOCK_PRACTITIONER_NAME, + authoredOn = Instant.fromEpochSeconds(123456), + acceptUntil = Instant.fromEpochSeconds(123456), + isDirectAssignment = false, + prescriptionChipInformation = MOCK_CHIP_INFO + ) + + private val MOCK_TASK_DETAIL_BUNDLE_01 = OrderUseCaseData.TaskDetailedBundle( + prescription = MOCK_PRESCRIPTION_01, + invoiceInfo = OrderUseCaseData.InvoiceInfo( + hasInvoice = true, + invoiceSentOn = Instant.fromEpochSeconds(123456) + ) + ) + + private val MOCK_TASK_DETAIL_BUNDLE_02 = OrderUseCaseData.TaskDetailedBundle( + prescription = MOCK_PRESCRIPTION_02, + invoiceInfo = OrderUseCaseData.InvoiceInfo( + hasInvoice = true, + invoiceSentOn = Instant.fromEpochSeconds(123456) + ) + ) + + internal val MOCK_ORDER_DETAIL = OrderUseCaseData.OrderDetail( + orderId = MOCK_ORDER_ID, + taskDetailedBundles = listOf( + MOCK_TASK_DETAIL_BUNDLE_01, + MOCK_TASK_DETAIL_BUNDLE_02 + ), + sentOn = Instant.fromEpochSeconds(123456), + pharmacy = OrderUseCaseData.Pharmacy(MOCK_PHARMACY_O1.name, ""), + hasUnreadMessages = false + ) + + internal val MOCK_ORDER_01 = OrderUseCaseData.Order( + orderId = MOCK_ORDER_ID, + prescriptions = listOf(null), + sentOn = Instant.fromEpochSeconds(123456), + pharmacy = OrderUseCaseData.Pharmacy(MOCK_PHARMACY_O1.name, ""), + hasUnreadMessages = true, + latestCommunicationMessage = null + ) + + internal val MOCK_DISP_REQ_COMMUNICATION_01 = Communication( + taskId = MOCK_TASK_ID_01, + communicationId = MOCK_COMMUNICATION_ID_01, + orderId = MOCK_ORDER_ID, + profile = CommunicationProfile.ErxCommunicationDispReq, + sentOn = Instant.fromEpochSeconds(123456), + sender = "sender1", + recipient = MOCK_PHARMACY_O1.name, + payload = "payload1", + consumed = true + ) + internal val MOCK_DISP_REPLY_COMMUNICATION_01 = MOCK_DISP_REQ_COMMUNICATION_01.copy( + profile = CommunicationProfile.ErxCommunicationReply, + consumed = false, + payload = MOCK_PAYLOAD + ) + internal val MOCK_DISP_REQ_COMMUNICATION_02 = Communication( + taskId = MOCK_TASK_ID_02, + communicationId = MOCK_COMMUNICATION_ID_02, + orderId = MOCK_ORDER_ID, + profile = CommunicationProfile.ErxCommunicationDispReq, + sentOn = Instant.fromEpochSeconds(123456), + sender = "sender2", + recipient = MOCK_PHARMACY_O2.name, + payload = "payload2", + consumed = true + ) + internal val MOCK_DISP_REPLY_COMMUNICATION_02 = MOCK_DISP_REQ_COMMUNICATION_02.copy( + profile = CommunicationProfile.ErxCommunicationReply, + consumed = false, + payload = MOCK_PAYLOAD_02 + ) + + private val MOCK_ORGANIZATION = SyncedTaskData.Organization( + name = "TestOrganization", + address = SyncedTaskData.Address( + line1 = "123 Main Street", + line2 = "Apt 4", + postalCode = "12345", + city = "City" + ), + uniqueIdentifier = "org123", + phone = "123-456-7890", + mail = "info@testorg.com" + ) + + private val MOCK_PATIENT = SyncedTaskData.Patient( + name = "Jane", + address = SyncedTaskData.Address( + line1 = "", + line2 = "", + postalCode = "", + city = "" + ), + birthdate = null, + insuranceIdentifier = "ins123" + ) + + private val MOCK_MEDICATION_REQ = SyncedTaskData.MedicationRequest( + null, null, null, SyncedTaskData.AccidentType.None, + null, null, false, null, + SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None + ) + + private fun mockChargeItem( + itemDescription: String, + itemFactor: Double, + itemPrice: Double + ): InvoiceData.ChargeableItem { + return InvoiceData.ChargeableItem( + description = InvoiceData.ChargeableItem.Description.PZN("PZN123"), + text = itemDescription, + factor = itemFactor, + price = InvoiceData.PriceComponent(value = itemPrice, tax = 0.0) + ) + } + + private val MOCK_INVOICE = InvoiceData.Invoice( + totalAdditionalFee = 10.0, + totalBruttoAmount = 120.0, + currency = "EUR", + chargeableItems = listOf( + mockChargeItem("Description1", 2.0, 30.0), + mockChargeItem("Description2", 1.5, 50.0) + ), + additionalDispenseItems = emptyList(), + additionalInformation = listOf("AdditionalInfo1", "AdditionalInfo2") + ) + + internal val MOCK_INVOICE_01 = InvoiceData.PKVInvoiceRecord( + profileId = "testProfileId", + taskId = MOCK_TASK_ID_01, + accessCode = "testAccessCode", + timestamp = Instant.fromEpochSeconds(123456), + pharmacyOrganization = MOCK_ORGANIZATION, + practitionerOrganization = MOCK_ORGANIZATION, + practitioner = MOCK_PRACTITIONER, + patient = MOCK_PATIENT, + medicationRequest = MOCK_MEDICATION_REQ, + whenHandedOver = null, + invoice = MOCK_INVOICE, + dmcPayload = "testDmcPayload", + consumed = false + ) + + internal val MOCK_INVOICE_02 = InvoiceData.PKVInvoiceRecord( + profileId = "testProfileId", + taskId = MOCK_TASK_ID_02, + accessCode = "testAccessCode", + timestamp = Instant.fromEpochSeconds(123456), + pharmacyOrganization = MOCK_ORGANIZATION, + practitionerOrganization = MOCK_ORGANIZATION, + practitioner = MOCK_PRACTITIONER, + patient = MOCK_PATIENT, + medicationRequest = MOCK_MEDICATION_REQ, + whenHandedOver = null, + invoice = MOCK_INVOICE, + dmcPayload = "testDmcPayload", + consumed = false + ) + + internal val MOCK_SYNCED_TASK_DATA_01 = SyncedTaskData.SyncedTask( + profileId = "testProfileId", + taskId = MOCK_TASK_ID_01, + accessCode = "testAccessCode", + lastModified = Instant.fromEpochSeconds(123456), + organization = MOCK_ORGANIZATION, + practitioner = MOCK_PRACTITIONER, + patient = MOCK_PATIENT, + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = "TestInsurance", + status = "Active", + coverageType = SyncedTaskData.CoverageType.GKV + ), + expiresOn = Instant.fromEpochSeconds(123456), + acceptUntil = Instant.fromEpochSeconds(123456), + authoredOn = Instant.fromEpochSeconds(123456), + status = SyncedTaskData.TaskStatus.Ready, + isIncomplete = false, + pvsIdentifier = "testPvsIdentifier", + failureToReport = "testFailureToReport", + medicationRequest = MOCK_MEDICATION_REQ, + lastMedicationDispense = null, + medicationDispenses = emptyList(), + communications = emptyList() + ) + + internal val MOCK_SYNCED_TASK_DATA_02 = SyncedTaskData.SyncedTask( + profileId = "testProfileId", + taskId = MOCK_TASK_ID_02, + accessCode = "testAccessCode", + lastModified = Instant.fromEpochSeconds(123456), + organization = MOCK_ORGANIZATION, + practitioner = MOCK_PRACTITIONER, + patient = MOCK_PATIENT, + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = "TestInsurance", + status = "Active", + coverageType = SyncedTaskData.CoverageType.GKV + ), + expiresOn = Instant.fromEpochSeconds(123456), + acceptUntil = Instant.fromEpochSeconds(123456), + authoredOn = Instant.fromEpochSeconds(123456), + status = SyncedTaskData.TaskStatus.Ready, + isIncomplete = false, + pvsIdentifier = "testPvsIdentifier", + failureToReport = "testFailureToReport", + medicationRequest = MOCK_MEDICATION_REQ, + lastMedicationDispense = null, + medicationDispenses = emptyList(), + communications = emptyList() + ) + + internal val MOCK_MESSAGE_01 = OrderUseCaseData.Message( + communicationId = MOCK_COMMUNICATION_ID_01, + sentOn = Instant.fromEpochSeconds(123456), + message = "mock message.", + pickUpCodeDMC = "Test_01___Rezept_01___abcdefg12345", + pickUpCodeHR = "T01__R01", + link = "https://www.tree.fm/forest/33", + consumed = false + ) + + internal val MOCK_MESSAGE_02 = OrderUseCaseData.Message( + communicationId = MOCK_COMMUNICATION_ID_02, + sentOn = Instant.fromEpochSeconds(123456), + message = "mock message_02.", + pickUpCodeDMC = "Test_01___Rezept_02___abcdefg12345", + pickUpCodeHR = "T01__R02", + link = "https://www.tree.fm/forest/35", + consumed = false + ) + + internal val MOCK_PROFILE = ProfilesData.Profile( + id = "1", + name = "Mustermann", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.FemaleDoctor, + insuranceIdentifier = "12345567890", + insuranceType = ProfilesData.InsuranceType.GKV, + insurantName = "Mustermann", + insuranceName = "GesundheitsVersichert AG", + singleSignOnTokenScope = null, + active = false, + isConsentDrawerShown = false, + lastAuthenticated = null + ) +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessageUsingOrderIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessageUsingOrderIdUseCaseTest.kt new file mode 100644 index 00000000..cbde887f --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessageUsingOrderIdUseCaseTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessageUsingOrderIdUseCase +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_DISP_REQ_COMMUNICATION_02 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_INVOICE_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_INVOICE_02 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_ORDER_DETAIL +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_ORDER_ID +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_PHARMACY_O1 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_PHARMACY_O2 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_02 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_02 +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class GetMessageUsingOrderIdUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + private val invoiceRepository: InvoiceRepository = mockk() + + @InjectMockKs + private lateinit var useCase: GetMessageUsingOrderIdUseCase + + @Before + fun setup() { + coEvery { + communicationRepository.loadDispReqCommunications(any()) + } returns flowOf(listOf(MOCK_DISP_REQ_COMMUNICATION_01, MOCK_DISP_REQ_COMMUNICATION_02)) + coEvery { communicationRepository.loadPharmacies() } returns flowOf(listOf(MOCK_PHARMACY_O1, MOCK_PHARMACY_O2)) + coEvery { communicationRepository.taskIdsByOrder(any()) } returns flowOf(listOf(MOCK_TASK_ID_01, MOCK_TASK_ID_02)) + coEvery { communicationRepository.hasUnreadDispenseMessage(any(), any()) } returns flowOf(false) + coEvery { communicationRepository.hasUnreadRepliedMessages(any(), any()) } returns flowOf(false) + coEvery { communicationRepository.loadSyncedByTaskId(MOCK_TASK_ID_01) } returns flowOf(MOCK_SYNCED_TASK_DATA_01) + coEvery { communicationRepository.loadSyncedByTaskId(MOCK_TASK_ID_02) } returns flowOf(MOCK_SYNCED_TASK_DATA_02) + coEvery { communicationRepository.downloadMissingPharmacy(any()) } returns Result.success(null) + coEvery { invoiceRepository.invoiceByTaskId(MOCK_TASK_ID_01) } returns flowOf(MOCK_INVOICE_01) + coEvery { invoiceRepository.invoiceByTaskId(MOCK_TASK_ID_02) } returns flowOf(MOCK_INVOICE_02) + + useCase = GetMessageUsingOrderIdUseCase( + communicationRepository = communicationRepository, + invoiceRepository = invoiceRepository, + dispatcher = dispatcher + ) + } + + @Test + fun `invoke should return order detail`() = runTest(dispatcher) { + val expectedOrderDetail = MOCK_ORDER_DETAIL + + val resultOrderDetail = useCase(MOCK_ORDER_ID).first() + + assertEquals(expectedOrderDetail, resultOrderDetail) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessagesUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessagesUseCaseTest.kt new file mode 100644 index 00000000..90e8e197 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetMessagesUseCaseTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_ORDER_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_PHARMACY_O1 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_PROFILE +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_01 +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessagesUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class GetMessagesUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + + private val profileRepository: ProfileRepository = mockk() + + private val invoiceRepository: InvoiceRepository = mockk() + + @InjectMockKs + private lateinit var useCase: GetMessagesUseCase + + @Before + fun setup() { + coEvery { profileRepository.profiles() } returns flowOf(listOf(MOCK_PROFILE)) + + coEvery { + communicationRepository.loadFirstDispReqCommunications(any()) + } returns flowOf(listOf(MOCK_DISP_REQ_COMMUNICATION_01)) + + coEvery { + communicationRepository.loadPharmacies() + } returns flowOf(listOf(MOCK_PHARMACY_O1)) + + coEvery { + communicationRepository.taskIdsByOrder(any()) + } returns flowOf(listOf(MOCK_TASK_ID_01)) + + coEvery { + communicationRepository.hasUnreadDispenseMessage(any(), any()) + } returns flowOf(true) + + coEvery { + communicationRepository.hasUnreadRepliedMessages(any(), any()) + } returns flowOf(true) + + coEvery { + communicationRepository.downloadMissingPharmacy(any()) + } returns Result.success(null) + + coEvery { + communicationRepository.loadSyncedByTaskId(any()) + } returns flowOf(null) + + coEvery { + communicationRepository.loadScannedByTaskId(any()) + } returns flowOf(null) + + coEvery { + communicationRepository.loadRepliedCommunications(any(), any()) + } returns flowOf(emptyList()) + coEvery { + communicationRepository.loadDispReqCommunications(any()) + } returns flowOf(emptyList()) + + coEvery { + invoiceRepository.getInvoiceTaskIdAndConsumedStatus(any()) + } returns flowOf(emptyList()) + + coEvery { invoiceRepository.hasUnreadInvoiceMessages(any()) } returns flowOf(false) + + useCase = GetMessagesUseCase( + communicationRepository = communicationRepository, + profileRepository = profileRepository, + invoiceRepository = invoiceRepository, + dispatcher = dispatcher + ) + } + + @Test + fun `invoke should return list of orders`() = runTest(dispatcher) { + val expectedOrders = listOf(MOCK_ORDER_01) + + val resultOrders: List = useCase() + + assertEquals(expectedOrders, resultOrders) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetRepliedMessagesUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetRepliedMessagesUseCaseTest.kt new file mode 100644 index 00000000..ffcdbf01 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetRepliedMessagesUseCaseTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_DISP_REPLY_COMMUNICATION_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_DISP_REPLY_COMMUNICATION_02 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_MESSAGE_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_MESSAGE_02 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_ORDER_ID +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_02 +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.domain.usecase.GetRepliedMessagesUseCase +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class GetRepliedMessagesUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + + @InjectMockKs + private lateinit var useCase: GetRepliedMessagesUseCase + + @Before + fun setup() { + coEvery { + communicationRepository.taskIdsByOrder(any()) + } returns flowOf(listOf(MOCK_TASK_ID_01, MOCK_TASK_ID_02)) + + coEvery { + communicationRepository.loadRepliedCommunications(any(), any()) + } returns flowOf(listOf(MOCK_DISP_REPLY_COMMUNICATION_01, MOCK_DISP_REPLY_COMMUNICATION_02)) + + useCase = GetRepliedMessagesUseCase( + communicationRepository = communicationRepository, + dispatcher = dispatcher + ) + } + + @Test + fun `invoke should return list of replied messages`() = runTest(dispatcher) { + val expectedRepliedMessages = listOf(MOCK_MESSAGE_01, MOCK_MESSAGE_02) + + val resultRepliedMessages: Flow> = + useCase(MOCK_ORDER_ID, "") + + assertEquals(expectedRepliedMessages, resultRepliedMessages.first()) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetUnreadMessagesCountUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetUnreadMessagesCountUseCaseTest.kt new file mode 100644 index 00000000..e527c9b4 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/GetUnreadMessagesCountUseCaseTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.domain.usecase.GetUnreadMessagesCountUseCase +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class GetUnreadMessagesCountUseCaseTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + private val inAppMessageRepository: InAppMessageRepository = mockk() + private val invoiceRepository: InvoiceRepository = mockk() + + private val profileId: ProfileIdentifier = "testProfileId" + + @InjectMockKs + private lateinit var useCase: GetUnreadMessagesCountUseCase + + @Before + fun setup() { + coEvery { communicationRepository.unreadMessagesCount(any()) } returns flowOf(15L) + coEvery { + invoiceRepository.getInvoiceTaskIdAndConsumedStatus(any()) + } returns flowOf(listOf(InvoiceData.InvoiceStatus(taskId = "taskId1", consumed = false))) + coEvery { communicationRepository.loadFirstDispReqCommunications(any()) } returns flowOf(emptyList()) + coEvery { communicationRepository.loadRepliedCommunications(any(), any()) } returns flowOf(emptyList()) + coEvery { inAppMessageRepository.counter } returns flowOf(0L) + useCase = GetUnreadMessagesCountUseCase(communicationRepository, inAppMessageRepository, invoiceRepository, dispatcher) + } + + @Test + fun `invoke should return flow of unread orders`() = runTest(dispatcher) { + val expectedUnreadCount = 15L + + val resultOrders = useCase(profileId).single() + + assertEquals(expectedUnreadCount, resultOrders) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/MessageUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/MessageUseCaseTest.kt new file mode 100644 index 00000000..bc4017cc --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/MessageUseCaseTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.messages.mappers.toMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.datetime.Instant +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals + +class MessageUseCaseTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + @Test + fun `communication to message - normal`() { + val communication = Communication( + taskId = "", + orderId = "", + communicationId = "CID123456", + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.fromEpochSeconds(123456), + sender = "ABC123456", + recipient = "ABC654321", + payload = """ + { + "version": 1, + "info_text": "Hi!", + "supplyOptionsType": "shipment", + "url": "https://example.org" + } + """.trimIndent(), + consumed = false + ) + val expected = OrderUseCaseData.Message( + communicationId = "CID123456", + sentOn = Instant.fromEpochSeconds(123456), + message = "Hi!", + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = "https://example.org", + consumed = false + ) + assertEquals(expected, communication.toMessage()) + } + + @Test + fun `communication to message - payload partially empty`() { + val communication = Communication( + taskId = "", + orderId = "", + communicationId = "CID123456", + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.fromEpochSeconds(123456), + sender = "ABC123456", + recipient = "ABC654321", + payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": " ", "pickUpCodeHR": "" }""", + consumed = false + ) + val expected = OrderUseCaseData.Message( + communicationId = "CID123456", + sentOn = Instant.fromEpochSeconds(123456), + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = false + ) + assertEquals(expected, communication.toMessage()) + } + + @Test + fun `communication to message - payload broken`() { + val communication = Communication( + taskId = "", + orderId = "", + communicationId = "CID123456", + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.fromEpochSeconds(123456), + sender = "ABC123456", + recipient = "ABC654321", + payload = """{ - """, + consumed = false + ) + val expected = OrderUseCaseData.Message( + communicationId = "CID123456", + sentOn = Instant.fromEpochSeconds(123456), + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = false + ) + assertEquals(expected, communication.toMessage()) + } + + @Test + fun `communication to message - invalid url`() { + val communication = Communication( + taskId = "", + orderId = "", + communicationId = "CID123456", + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.fromEpochSeconds(123456), + sender = "ABC123456", + recipient = "ABC654321", + payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": "ftp://example.org" }""", + consumed = false + ) + val expected = OrderUseCaseData.Message( + communicationId = "CID123456", + sentOn = Instant.fromEpochSeconds(123456), + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = false + ) + assertEquals(expected, communication.toMessage()) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/SaveLocalCommunicationUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/SaveLocalCommunicationUseCaseTest.kt new file mode 100644 index 00000000..601747d3 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/SaveLocalCommunicationUseCaseTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.messages.domain.usecase.SaveLocalCommunicationUseCase +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_PHARMACY_O1 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TASK_ID_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_TRANSACTION_ID +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class SaveLocalCommunicationUseCaseTest { + + @get:Rule + val coroutineRule = CoroutineTestRule() + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + + @InjectMockKs + private lateinit var useCase: SaveLocalCommunicationUseCase + + @Before + fun setup() { + coEvery { + communicationRepository.saveLocalCommunication( + MOCK_TASK_ID_01, + MOCK_PHARMACY_O1.telematikId, + MOCK_TRANSACTION_ID + ) + } returns Unit + + useCase = SaveLocalCommunicationUseCase( + repository = communicationRepository, + dispatcher = dispatcher + ) + } + + @Test + fun `invoke should save local communication`() = runTest(dispatcher) { + val mockTaskId = MOCK_TASK_ID_01 + val mockPharmacyId = MOCK_PHARMACY_O1.telematikId + + useCase.invoke(mockTaskId, mockPharmacyId, MOCK_TRANSACTION_ID) + + coVerify(exactly = 1) { + communicationRepository.saveLocalCommunication(mockTaskId, mockPharmacyId, MOCK_TRANSACTION_ID) + } + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt new file mode 100644 index 00000000..5b020ced --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.mocks.MessageMocks +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class UpdateCommunicationByCommunicationIdUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val communicationRepository: CommunicationRepository = mockk() + + @InjectMockKs + private lateinit var useCase: UpdateCommunicationByCommunicationIdUseCase + + @Before + fun setup() { + useCase = UpdateCommunicationByCommunicationIdUseCase( + repository = communicationRepository, + dispatcher = dispatcher + ) + } + + @Test + fun `invoke should update communication status`() = runTest(dispatcher) { + val communicationId = MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01.communicationId + coEvery { + communicationRepository.setCommunicationStatus(communicationId, true) + } returns Unit + + useCase(communicationId) + + coVerify(exactly = 1) { + communicationRepository.setCommunicationStatus(communicationId, true) + } + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt new file mode 100644 index 00000000..fb1ca1e3 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/messages/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.usecase + +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByOrderIdAndCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.mocks.MessageMocks +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class UpdateCommunicationByOrderIdUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val repository: CommunicationRepository = mockk() + + @InjectMockKs + private lateinit var useCase: UpdateCommunicationByOrderIdAndCommunicationIdUseCase + + @Before + fun setup() { + coEvery { + repository.loadDispReqCommunications(any()) + } returns flowOf( + listOf( + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01, + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_02 + ) + ) + + coEvery { + repository.setCommunicationStatus(any(), any()) + } returns Unit + + useCase = UpdateCommunicationByOrderIdAndCommunicationIdUseCase(repository, dispatcher) + } + + @Test + fun `invoke should update communication status`() = runTest(dispatcher) { + val communicationIdsCaptured = mutableListOf() + + coEvery { + repository.loadDispReqCommunications(any()) + } returns flowOf( + listOf( + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01, + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_02 + ) + ) + + coEvery { + repository.setCommunicationStatus(capture(communicationIdsCaptured), true) + } returns Unit + + useCase(MessageMocks.MOCK_ORDER_ID) + + assertEquals( + communicationIdsCaptured, + listOf( + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_01.communicationId, + MessageMocks.MOCK_DISP_REQ_COMMUNICATION_02.communicationId + ) + ) + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt deleted file mode 100644 index 154e2ecc..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard.usecase - -import org.junit.Test -import kotlin.test.assertEquals - -private val contacts = """ - [ - { - "name":"Kasse 1", - "healthCardAndPinPhone":"+123123", - "healthCardAndPinMail":"TestMail@test.de", - "healthCardAndPinUrl":"https://www.TestURL.de/", - "pinUrl":"https://www.TestPinURL.de/", - "subjectCardAndPinMail":"testHeader", - "bodyCardAndPinMail":"testBody", - "subjectPinMail":"testHeader", - "bodyPinMail":"testBody" - }, - { - "name":"Kasse 2", - "healthCardAndPinPhone":null, - "healthCardAndPinMail":null, - "healthCardAndPinUrl":null, - "pinUrl":null, - "subjectCardAndPinMail":null, - "bodyCardAndPinMail":null, - "subjectPinMail":null, - "bodyPinMail":null - } - ] -""".trimIndent() - -class HealthCardOrderUseCaseTest { - - @Test - fun `test loadHealthInsuranceContactsFromCSV() with expected data`() { - loadHealthInsuranceContactsFromJSON(contacts.byteInputStream()).let { - assertEquals("Kasse 1", it[0].name) - assertEquals("TestMail@test.de", it[0].healthCardAndPinMail) - assertEquals("+123123", it[0].healthCardAndPinPhone) - assertEquals("https://www.TestURL.de/", it[0].healthCardAndPinUrl) - assertEquals("https://www.TestPinURL.de/", it[0].pinUrl) - assertEquals("testHeader", it[0].subjectCardAndPinMail) - assertEquals("testBody", it[0].bodyCardAndPinMail) - assertEquals("testHeader", it[0].subjectPinMail) - assertEquals("testBody", it[0].bodyPinMail) - - assertEquals("Kasse 2", it[1].name) - assertEquals(null, it[1].healthCardAndPinMail) - assertEquals(null, it[1].healthCardAndPinPhone) - assertEquals(null, it[1].healthCardAndPinUrl) - assertEquals(null, it[1].pinUrl) - assertEquals(null, it[1].subjectCardAndPinMail) - assertEquals(null, it[1].bodyCardAndPinMail) - assertEquals(null, it[1].subjectPinMail) - assertEquals(null, it[1].bodyPinMail) - } - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCaseTest.kt new file mode 100644 index 00000000..d14f3e35 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCaseTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.usecase + +import org.junit.Test +import kotlin.test.assertEquals + +private val contacts = """ + [ + { + "name":"Kasse 1", + "healthCardAndPinPhone":"+123123", + "healthCardAndPinMail":"TestMail@test.de", + "healthCardAndPinUrl":"https://www.TestURL.de/", + "pinUrl":"https://www.TestPinURL.de/", + "subjectCardAndPinMail":"testHeader", + "bodyCardAndPinMail":"testBody", + "subjectPinMail":"testHeader", + "bodyPinMail":"testBody" + }, + { + "name":"Kasse 2", + "healthCardAndPinPhone":null, + "healthCardAndPinMail":null, + "healthCardAndPinUrl":null, + "pinUrl":null, + "subjectCardAndPinMail":null, + "bodyCardAndPinMail":null, + "subjectPinMail":null, + "bodyPinMail":null + } + ] +""".trimIndent() + +class LoadHealthInsuranceListUseCaseTest { + + @Test + fun `test loadHealthInsuranceContactsFromCSV() with expected data`() { + loadHealthInsuranceContactsFromJSON(contacts.byteInputStream()).let { + assertEquals("Kasse 1", it[0].name) + assertEquals("TestMail@test.de", it[0].healthCardAndPinMail) + assertEquals("+123123", it[0].healthCardAndPinPhone) + assertEquals("https://www.TestURL.de/", it[0].healthCardAndPinUrl) + assertEquals("https://www.TestPinURL.de/", it[0].pinUrl) + assertEquals("testHeader", it[0].subjectCardAndPinMail) + assertEquals("testBody", it[0].bodyCardAndPinMail) + assertEquals("testHeader", it[0].subjectPinMail) + assertEquals("testBody", it[0].bodyPinMail) + + assertEquals("Kasse 2", it[1].name) + assertEquals(null, it[1].healthCardAndPinMail) + assertEquals(null, it[1].healthCardAndPinPhone) + assertEquals(null, it[1].healthCardAndPinUrl) + assertEquals(null, it[1].pinUrl) + assertEquals(null, it[1].subjectCardAndPinMail) + assertEquals(null, it[1].bodyCardAndPinMail) + assertEquals(null, it[1].subjectPinMail) + assertEquals(null, it[1].bodyPinMail) + } + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/mocks/OrderMocks.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/mocks/OrderMocks.kt deleted file mode 100644 index 43a7717b..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/mocks/OrderMocks.kt +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.mocks - -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.orders.repository.CachedPharmacy -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.model.CommunicationProfile -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription -import kotlinx.datetime.Instant - -object OrderMocks { - - internal const val MOCK_ORDER_ID = "testOrderId" - - internal const val MOCK_TASK_ID_01 = "123-001" - - internal const val MOCK_TASK_ID_02 = "456-002" - - private const val MOCK_COMMUNICATION_ID_01 = "CID-123-001" - - private const val MOCK_COMMUNICATION_ID_02 = "CID-456-002" - - internal const val MOCK_TRANSACTION_ID = "transactionMockId" - - internal val MOCK_PHARMACY_O1 = CachedPharmacy(name = "Pharmacy1", telematikId = "123") - - internal val MOCK_PHARMACY_O2 = CachedPharmacy(name = "Pharmacy2", telematikId = "456") - - private const val MOCK_PRACTITIONER_NAME = "Dr. John Doe" - - internal const val MOCK_PROFILE_IDENTIFIER = "testProfileIdentifier" - - private val MOCK_PAYLOAD = """ - { - "version":1 , - "supplyOptionsType":"onPremise" , - "info_text":"mock message." , - "pickUpCodeHR":"T01__R01" , - "pickUpCodeDMC":"Test_01___Rezept_01___abcdefg12345" , - "url":"https://www.tree.fm/forest/33" - } - """.trimIndent() - - private val MOCK_PAYLOAD_02 = """ - { - "version":1 , - "supplyOptionsType":"onPremise" , - "info_text":"mock message_02." , - "pickUpCodeHR":"T01__R02" , - "pickUpCodeDMC":"Test_01___Rezept_02___abcdefg12345" , - "url":"https://www.tree.fm/forest/35" - } - """.trimIndent() - - private val MOCK_PRACTITIONER = SyncedTaskData.Practitioner( - name = MOCK_PRACTITIONER_NAME, - qualification = "", - practitionerIdentifier = " " - ) - - private val MOCK_CHIP_INFO = Prescription.PrescriptionChipInformation( - isPartOfMultiplePrescription = false, - numerator = null, - denominator = null, - start = null - ) - - private val MOCK_PRESCRIPTION_01 = Prescription.SyncedPrescription( - taskId = MOCK_TASK_ID_01, - name = null, - redeemedOn = null, - expiresOn = Instant.fromEpochSeconds(123456), - state = SyncedTaskData.SyncedTask.Expired( - expiredOn = Instant.fromEpochSeconds(123456) - ), - isIncomplete = false, - organization = MOCK_PRACTITIONER_NAME, - authoredOn = Instant.fromEpochSeconds(123456), - acceptUntil = Instant.fromEpochSeconds(123456), - isDirectAssignment = false, - prescriptionChipInformation = MOCK_CHIP_INFO - ) - - private val MOCK_PRESCRIPTION_02 = Prescription.SyncedPrescription( - taskId = MOCK_TASK_ID_02, - name = null, - redeemedOn = null, - expiresOn = Instant.fromEpochSeconds(123456), - state = SyncedTaskData.SyncedTask.Expired( - expiredOn = Instant.fromEpochSeconds(123456) - ), - isIncomplete = false, - organization = MOCK_PRACTITIONER_NAME, - authoredOn = Instant.fromEpochSeconds(123456), - acceptUntil = Instant.fromEpochSeconds(123456), - isDirectAssignment = false, - prescriptionChipInformation = MOCK_CHIP_INFO - ) - - private val MOCK_TASK_DETAIL_BUNDLE_01 = OrderUseCaseData.TaskDetailedBundle( - taskId = MOCK_TASK_ID_01, - prescription = MOCK_PRESCRIPTION_01, - hasInvoice = true - ) - - private val MOCK_TASK_DETAIL_BUNDLE_02 = OrderUseCaseData.TaskDetailedBundle( - taskId = MOCK_TASK_ID_02, - prescription = MOCK_PRESCRIPTION_02, - hasInvoice = true - ) - - internal val MOCK_ORDER_DETAIL = OrderUseCaseData.OrderDetail( - orderId = MOCK_ORDER_ID, - taskDetailedBundles = listOf( - MOCK_TASK_DETAIL_BUNDLE_01, - MOCK_TASK_DETAIL_BUNDLE_02 - ), - sentOn = Instant.fromEpochSeconds(123456), - pharmacy = OrderUseCaseData.Pharmacy(MOCK_PHARMACY_O1.name, ""), - hasUnreadMessages = true - ) - - internal val MOCK_ORDER_01 = OrderUseCaseData.Order( - orderId = MOCK_ORDER_ID, - taskIds = listOf(MOCK_TASK_ID_01), - prescriptions = emptyList(), - sentOn = Instant.fromEpochSeconds(123456), - pharmacy = OrderUseCaseData.Pharmacy(MOCK_PHARMACY_O1.name, ""), - hasUnreadMessages = true - ) - - internal val MOCK_DISP_REQ_COMMUNICATION_01 = Communication( - taskId = MOCK_TASK_ID_01, - communicationId = MOCK_COMMUNICATION_ID_01, - orderId = MOCK_ORDER_ID, - profile = CommunicationProfile.ErxCommunicationDispReq, - sentOn = Instant.fromEpochSeconds(123456), - sender = "sender1", - recipient = MOCK_PHARMACY_O1.name, - payload = "payload1", - consumed = true - ) - internal val MOCK_DISP_REPLY_COMMUNICATION_01 = MOCK_DISP_REQ_COMMUNICATION_01.copy( - profile = CommunicationProfile.ErxCommunicationReply, - consumed = false, - payload = MOCK_PAYLOAD - ) - internal val MOCK_DISP_REQ_COMMUNICATION_02 = Communication( - taskId = MOCK_TASK_ID_02, - communicationId = MOCK_COMMUNICATION_ID_02, - orderId = MOCK_ORDER_ID, - profile = CommunicationProfile.ErxCommunicationDispReq, - sentOn = Instant.fromEpochSeconds(123456), - sender = "sender2", - recipient = MOCK_PHARMACY_O2.name, - payload = "payload2", - consumed = true - ) - internal val MOCK_DISP_REPLY_COMMUNICATION_02 = MOCK_DISP_REQ_COMMUNICATION_02.copy( - profile = CommunicationProfile.ErxCommunicationReply, - consumed = false, - payload = MOCK_PAYLOAD_02 - ) - - private val MOCK_ORGANIZATION = SyncedTaskData.Organization( - name = "TestOrganization", - address = SyncedTaskData.Address( - line1 = "123 Main Street", - line2 = "Apt 4", - postalCode = "12345", - city = "City" - ), - uniqueIdentifier = "org123", - phone = "123-456-7890", - mail = "info@testorg.com" - ) - - private val MOCK_PATIENT = SyncedTaskData.Patient( - name = "Jane", - address = SyncedTaskData.Address( - line1 = "", - line2 = "", - postalCode = "", - city = "" - ), - birthdate = null, - insuranceIdentifier = "ins123" - ) - - private val MOCK_MEDICATION_REQ = SyncedTaskData.MedicationRequest( - null, null, null, SyncedTaskData.AccidentType.None, - null, null, false, null, - SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None - ) - - private fun mockChargeItem( - itemDescription: String, - itemFactor: Double, - itemPrice: Double - ): InvoiceData.ChargeableItem { - return InvoiceData.ChargeableItem( - description = InvoiceData.ChargeableItem.Description.PZN("PZN123"), - text = itemDescription, - factor = itemFactor, - price = InvoiceData.PriceComponent(value = itemPrice, tax = 0.0) - ) - } - - private val MOCK_INVOICE = InvoiceData.Invoice( - totalAdditionalFee = 10.0, - totalBruttoAmount = 120.0, - currency = "EUR", - chargeableItems = listOf( - mockChargeItem("Description1", 2.0, 30.0), - mockChargeItem("Description2", 1.5, 50.0) - ), - additionalDispenseItems = emptyList(), - additionalInformation = listOf("AdditionalInfo1", "AdditionalInfo2") - ) - - internal val MOCK_INVOICE_01 = InvoiceData.PKVInvoice( - profileId = "testProfileId", - taskId = MOCK_TASK_ID_01, - accessCode = "testAccessCode", - timestamp = Instant.fromEpochSeconds(123456), - pharmacyOrganization = MOCK_ORGANIZATION, - practitionerOrganization = MOCK_ORGANIZATION, - practitioner = MOCK_PRACTITIONER, - patient = MOCK_PATIENT, - medicationRequest = MOCK_MEDICATION_REQ, - whenHandedOver = null, - invoice = MOCK_INVOICE, - dmcPayload = "testDmcPayload" - ) - - internal val MOCK_INVOICE_02 = InvoiceData.PKVInvoice( - profileId = "testProfileId", - taskId = MOCK_TASK_ID_02, - accessCode = "testAccessCode", - timestamp = Instant.fromEpochSeconds(123456), - pharmacyOrganization = MOCK_ORGANIZATION, - practitionerOrganization = MOCK_ORGANIZATION, - practitioner = MOCK_PRACTITIONER, - patient = MOCK_PATIENT, - medicationRequest = MOCK_MEDICATION_REQ, - whenHandedOver = null, - invoice = MOCK_INVOICE, - dmcPayload = "testDmcPayload" - ) - - internal val MOCK_SYNCED_TASK_DATA_01 = SyncedTaskData.SyncedTask( - profileId = "testProfileId", - taskId = MOCK_TASK_ID_01, - accessCode = "testAccessCode", - lastModified = Instant.fromEpochSeconds(123456), - organization = MOCK_ORGANIZATION, - practitioner = MOCK_PRACTITIONER, - patient = MOCK_PATIENT, - insuranceInformation = SyncedTaskData.InsuranceInformation( - name = "TestInsurance", - status = "Active" - ), - expiresOn = Instant.fromEpochSeconds(123456), - acceptUntil = Instant.fromEpochSeconds(123456), - authoredOn = Instant.fromEpochSeconds(123456), - status = SyncedTaskData.TaskStatus.Ready, - isIncomplete = false, - pvsIdentifier = "testPvsIdentifier", - failureToReport = "testFailureToReport", - medicationRequest = MOCK_MEDICATION_REQ, - medicationDispenses = emptyList(), - communications = emptyList() - ) - - internal val MOCK_SYNCED_TASK_DATA_02 = SyncedTaskData.SyncedTask( - profileId = "testProfileId", - taskId = MOCK_TASK_ID_02, - accessCode = "testAccessCode", - lastModified = Instant.fromEpochSeconds(123456), - organization = MOCK_ORGANIZATION, - practitioner = MOCK_PRACTITIONER, - patient = MOCK_PATIENT, - insuranceInformation = SyncedTaskData.InsuranceInformation( - name = "TestInsurance", - status = "Active" - ), - expiresOn = Instant.fromEpochSeconds(123456), - acceptUntil = Instant.fromEpochSeconds(123456), - authoredOn = Instant.fromEpochSeconds(123456), - status = SyncedTaskData.TaskStatus.Ready, - isIncomplete = false, - pvsIdentifier = "testPvsIdentifier", - failureToReport = "testFailureToReport", - medicationRequest = MOCK_MEDICATION_REQ, - medicationDispenses = emptyList(), - communications = emptyList() - ) - - internal val MOCK_MESSAGE_01 = OrderUseCaseData.Message( - communicationId = MOCK_COMMUNICATION_ID_01, - sentOn = Instant.fromEpochSeconds(123456), - message = "mock message.", - code = "Test_01___Rezept_01___abcdefg12345", - link = "https://www.tree.fm/forest/33", - consumed = false, - hasInvoice = true - ) - - internal val MOCK_MESSAGE_02 = OrderUseCaseData.Message( - communicationId = MOCK_COMMUNICATION_ID_02, - sentOn = Instant.fromEpochSeconds(123456), - message = "mock message_02.", - code = "Test_01___Rezept_02___abcdefg12345", - link = "https://www.tree.fm/forest/35", - consumed = false, - hasInvoice = true - ) -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingOrderIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingOrderIdUseCaseTest.kt deleted file mode 100644 index bd0c1e4a..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingOrderIdUseCaseTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_DISP_REQ_COMMUNICATION_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_INVOICE_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_INVOICE_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_ORDER_DETAIL -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_ORDER_ID -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_PHARMACY_O1 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_PHARMACY_O2 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_SYNCED_TASK_DATA_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_SYNCED_TASK_DATA_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_02 -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import io.mockk.coEvery -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class GetOrdersUsingOrderIdUseCaseTest { - - private val dispatcher = StandardTestDispatcher() - - private val communicationRepository: CommunicationRepository = mockk() - private val invoiceRepository: InvoiceRepository = mockk() - - @InjectMockKs - private lateinit var useCase: GetOrderUsingOrderIdUseCase - - @Before - fun setup() { - coEvery { - communicationRepository.loadDispReqCommunications(any()) - } returns flowOf(listOf(MOCK_DISP_REQ_COMMUNICATION_01, MOCK_DISP_REQ_COMMUNICATION_02)) - - coEvery { - communicationRepository.loadPharmacies() - } returns flowOf(listOf(MOCK_PHARMACY_O1, MOCK_PHARMACY_O2)) - - coEvery { - communicationRepository.taskIdsByOrder(any()) - } returns flowOf(listOf(MOCK_TASK_ID_01, MOCK_TASK_ID_02)) - - coEvery { - communicationRepository.hasUnreadPrescription(any(), any()) - } returns flowOf(true) - - coEvery { - communicationRepository.loadSyncedByTaskId(MOCK_TASK_ID_01) - } returns flowOf(MOCK_SYNCED_TASK_DATA_01) - - coEvery { - communicationRepository.loadSyncedByTaskId(MOCK_TASK_ID_02) - } returns flowOf(MOCK_SYNCED_TASK_DATA_02) - - coEvery { - communicationRepository.downloadMissingPharmacy(any()) - } returns Unit - - coEvery { invoiceRepository.invoiceById(MOCK_TASK_ID_01) } returns flowOf(MOCK_INVOICE_01) - - coEvery { invoiceRepository.invoiceById(MOCK_TASK_ID_02) } returns flowOf(MOCK_INVOICE_02) - - useCase = GetOrderUsingOrderIdUseCase( - communicationRepository = communicationRepository, - invoiceRepository = invoiceRepository, - dispatcher = dispatcher - ) - } - - @Test - fun `invoke should return order detail`() = runTest(dispatcher) { - val expectedOrderDetail = MOCK_ORDER_DETAIL - - val resultOrderDetail = useCase(MOCK_ORDER_ID) - - assertEquals(expectedOrderDetail, resultOrderDetail.first()) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCaseTest.kt deleted file mode 100644 index 862ef0d6..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCaseTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_ORDER_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_PHARMACY_O1 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_PROFILE_IDENTIFIER -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_01 -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import io.mockk.coEvery -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class GetOrdersUsingProfileIdUseCaseTest { - - private val dispatcher = StandardTestDispatcher() - - private val communicationRepository: CommunicationRepository = mockk() - - @InjectMockKs - private lateinit var useCase: GetOrdersUsingProfileIdUseCase - - @Before - fun setup() { - coEvery { - communicationRepository.loadFirstDispReqCommunications(any()) - } returns flowOf(listOf(MOCK_DISP_REQ_COMMUNICATION_01)) - - coEvery { - communicationRepository.loadPharmacies() - } returns flowOf(listOf(MOCK_PHARMACY_O1)) - - coEvery { - communicationRepository.taskIdsByOrder(any()) - } returns flowOf(listOf(MOCK_TASK_ID_01)) - - coEvery { - communicationRepository.hasUnreadPrescription(any(), any()) - } returns flowOf(true) - - coEvery { - communicationRepository.downloadMissingPharmacy(any()) - } returns Unit - - useCase = GetOrdersUsingProfileIdUseCase( - repository = communicationRepository, - dispatcher = dispatcher - ) - } - - @Test - fun `invoke should return list of orders`() = runTest(dispatcher) { - val expectedOrders = listOf(MOCK_ORDER_01) - - val resultOrders: Flow> = useCase(MOCK_PROFILE_IDENTIFIER) - - assertEquals(expectedOrders, resultOrders.first()) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCaseTest.kt deleted file mode 100644 index eda9ecec..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCaseTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_DISP_REPLY_COMMUNICATION_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_DISP_REPLY_COMMUNICATION_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_INVOICE_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_INVOICE_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_MESSAGE_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_MESSAGE_02 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_ORDER_ID -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_02 -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import io.mockk.coEvery -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class GetRepliedMessagesUseCaseTest { - - private val dispatcher = StandardTestDispatcher() - - private val communicationRepository: CommunicationRepository = mockk() - private val invoiceRepository: InvoiceRepository = mockk() - - @InjectMockKs - private lateinit var useCase: GetRepliedMessagesUseCase - - @Before - fun setup() { - coEvery { - communicationRepository.taskIdsByOrder(any()) - } returns flowOf(listOf(MOCK_TASK_ID_01, MOCK_TASK_ID_02)) - - coEvery { - communicationRepository.loadRepliedCommunications(any()) - } returns flowOf(listOf(MOCK_DISP_REPLY_COMMUNICATION_01, MOCK_DISP_REPLY_COMMUNICATION_02)) - - coEvery { - invoiceRepository.invoiceById(MOCK_TASK_ID_01) - } returns flowOf(MOCK_INVOICE_01) - - coEvery { - invoiceRepository.invoiceById(MOCK_TASK_ID_02) - } returns flowOf(MOCK_INVOICE_02) - - useCase = GetRepliedMessagesUseCase( - communicationRepository = communicationRepository, - invoiceRepository = invoiceRepository, - dispatcher = dispatcher - ) - } - - @Test - fun `invoke should return list of replied messages`() = runTest(dispatcher) { - val expectedRepliedMessages = listOf(MOCK_MESSAGE_01, MOCK_MESSAGE_02) - - val resultRepliedMessages: Flow> = - useCase(MOCK_ORDER_ID) - - assertEquals(expectedRepliedMessages, resultRepliedMessages.first()) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCaseTest.kt deleted file mode 100644 index 518f9128..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCaseTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import io.mockk.coEvery -import io.mockk.every -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.single -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class GetUnreadOrdersUseCaseTest { - @get:Rule - val coroutineRule = CoroutineTestRule() - - private val dispatcher = StandardTestDispatcher() - - private val repository: CommunicationRepository = mockk() - - @InjectMockKs - private lateinit var useCase: GetUnreadOrdersUseCase - - private val profile: ProfilesUseCaseData.Profile = mockk() - - @Before - fun setup() { - coEvery { repository.unreadOrders(any()) } returns flowOf(10L) - every { profile.id } returns "12" - - useCase = GetUnreadOrdersUseCase(repository, dispatcher) - } - - @Test - fun `invoke should return flow of unread orders`() = runTest(dispatcher) { - val expectedUnreadCount = 10L - - val resultOrders = useCase(profile.id).single() - - assertEquals(expectedUnreadCount, resultOrders) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt deleted file mode 100644 index 96aa488a..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.orders.mappers.toMessage -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.model.CommunicationProfile -import kotlinx.datetime.Instant -import org.junit.Rule -import kotlin.test.Test -import kotlin.test.assertEquals - -class OrderUseCaseTest { - @get:Rule - val coroutineRule = CoroutineTestRule() - - @Test - fun `communication to message - normal`() { - val communication = Communication( - taskId = "", - orderId = "", - communicationId = "CID123456", - profile = CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.fromEpochSeconds(123456), - sender = "ABC123456", - recipient = "ABC654321", - payload = """ - { - "version": 1, - "info_text": "Hi!", - "supplyOptionsType": "shipment", - "url": "https://example.org" - } - """.trimIndent(), - consumed = false - ) - val expected = OrderUseCaseData.Message( - communicationId = "CID123456", - sentOn = Instant.fromEpochSeconds(123456), - message = "Hi!", - code = null, - link = "https://example.org", - consumed = false, - hasInvoice = false - ) - assertEquals(expected, communication.toMessage()) - } - - @Test - fun `communication to message - payload partially empty`() { - val communication = Communication( - taskId = "", - orderId = "", - communicationId = "CID123456", - profile = CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.fromEpochSeconds(123456), - sender = "ABC123456", - recipient = "ABC654321", - payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": " ", "pickUpCodeHR": "" }""", - consumed = false - ) - val expected = OrderUseCaseData.Message( - communicationId = "CID123456", - sentOn = Instant.fromEpochSeconds(123456), - message = null, - code = null, - link = null, - consumed = false, - hasInvoice = false - ) - assertEquals(expected, communication.toMessage()) - } - - @Test - fun `communication to message - payload broken`() { - val communication = Communication( - taskId = "", - orderId = "", - communicationId = "CID123456", - profile = CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.fromEpochSeconds(123456), - sender = "ABC123456", - recipient = "ABC654321", - payload = """{ - """, - consumed = false - ) - val expected = OrderUseCaseData.Message( - communicationId = "CID123456", - sentOn = Instant.fromEpochSeconds(123456), - message = null, - code = null, - link = null, - consumed = false, - hasInvoice = false - ) - assertEquals(expected, communication.toMessage()) - } - - @Test - fun `communication to message - invalid url`() { - val communication = Communication( - taskId = "", - orderId = "", - communicationId = "CID123456", - profile = CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.fromEpochSeconds(123456), - sender = "ABC123456", - recipient = "ABC654321", - payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": "ftp://example.org" }""", - consumed = false - ) - val expected = OrderUseCaseData.Message( - communicationId = "CID123456", - sentOn = Instant.fromEpochSeconds(123456), - message = null, - code = null, - link = null, - consumed = false, - hasInvoice = false - ) - assertEquals(expected, communication.toMessage()) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCaseTest.kt deleted file mode 100644 index cb21a060..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCaseTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_PHARMACY_O1 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TASK_ID_01 -import de.gematik.ti.erp.app.orders.mocks.OrderMocks.MOCK_TRANSACTION_ID -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class SaveLocalCommunicationUseCaseTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - - private val dispatcher = StandardTestDispatcher() - - private val communicationRepository: CommunicationRepository = mockk() - - @InjectMockKs - private lateinit var useCase: SaveLocalCommunicationUseCase - - @Before - fun setup() { - coEvery { - communicationRepository.saveLocalCommunication( - MOCK_TASK_ID_01, - MOCK_PHARMACY_O1.telematikId, - MOCK_TRANSACTION_ID - ) - } returns Unit - - useCase = SaveLocalCommunicationUseCase( - repository = communicationRepository, - dispatcher = dispatcher - ) - } - - @Test - fun `invoke should save local communication`() = runTest(dispatcher) { - val mockTaskId = MOCK_TASK_ID_01 - val mockPharmacyId = MOCK_PHARMACY_O1.telematikId - - useCase.invoke(mockTaskId, mockPharmacyId, MOCK_TRANSACTION_ID) - - coVerify(exactly = 1) { - communicationRepository.saveLocalCommunication(mockTaskId, mockPharmacyId, MOCK_TRANSACTION_ID) - } - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt deleted file mode 100644 index 1e7772ae..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCaseTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.mocks.OrderMocks -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -@ExperimentalCoroutinesApi -class UpdateCommunicationByCommunicationIdUseCaseTest { - - private val dispatcher = StandardTestDispatcher() - - private val communicationRepository: CommunicationRepository = mockk() - - @InjectMockKs - private lateinit var useCase: UpdateCommunicationByCommunicationIdUseCase - - @Before - fun setup() { - useCase = UpdateCommunicationByCommunicationIdUseCase( - repository = communicationRepository, - dispatcher = dispatcher - ) - } - - @Test - fun `invoke should update communication status`() = runTest(dispatcher) { - val communicationId = OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01.communicationId - coEvery { - communicationRepository.setCommunicationStatus(communicationId, true) - } returns Unit - - useCase(communicationId) - - coVerify(exactly = 1) { - communicationRepository.setCommunicationStatus(communicationId, true) - } - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt deleted file mode 100644 index fe155b8f..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCaseTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.mocks.OrderMocks -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import io.mockk.coEvery -import io.mockk.impl.annotations.InjectMockKs -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class UpdateCommunicationByOrderIdUseCaseTest { - - private val dispatcher = StandardTestDispatcher() - - private val repository: CommunicationRepository = mockk() - - @InjectMockKs - private lateinit var useCase: UpdateCommunicationByOrderIdUseCase - - @Before - fun setup() { - coEvery { - repository.loadDispReqCommunications(any()) - } returns flowOf( - listOf( - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01, - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_02 - ) - ) - - coEvery { - repository.setCommunicationStatus(any(), any()) - } returns Unit - - useCase = UpdateCommunicationByOrderIdUseCase(repository, dispatcher) - } - - @Test - fun `invoke should update communication status`() = runTest(dispatcher) { - val communicationIdsCaptured = mutableListOf() - - coEvery { - repository.loadDispReqCommunications(any()) - } returns flowOf( - listOf( - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01, - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_02 - ) - ) - - coEvery { - repository.setCommunicationStatus(capture(communicationIdsCaptured), true) - } returns Unit - - useCase(OrderMocks.MOCK_ORDER_ID) - - assertEquals( - communicationIdsCaptured, - listOf( - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_01.communicationId, - OrderMocks.MOCK_DISP_REQ_COMMUNICATION_02.communicationId - ) - ) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt index f8982198..28945cb2 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.util.encoders.Base64 -import kotlin.test.Test +import org.junit.Test import kotlin.test.assertEquals private val testCert = """ diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/mocks/PharmacyMocks.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/mocks/PharmacyMocks.kt new file mode 100644 index 00000000..f4249f6a --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/mocks/PharmacyMocks.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.mocks + +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_02 +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.days + +internal val MOCK_ACTIVE_PROFILE = ProfilesData.Profile( + id = "1", + name = "Mustermann", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.FemaleDoctor, + insuranceIdentifier = "12345567890", + insuranceType = ProfilesData.InsuranceType.GKV, + insurantName = "Mustermann", + insuranceName = "GesundheitsVersichert AG", + singleSignOnTokenScope = null, + active = false, + isConsentDrawerShown = false, + lastAuthenticated = null +) + +internal val MOCK_SHIPPING_CONTACT = PharmacyData.ShippingContact( + name = "Max Mustermann", + line1 = "Musterstraße 1", + line2 = "", + postalCode = "12345", + city = "Musterstadt", + telephoneNumber = "0123456789", + mail = "", + deliveryInformation = "" +) + +internal val MOCK_SCANNED_TASK_DATA_REDEEMABLE_01 = ScannedTaskData.ScannedTask( + index = 1, + name = "ScannedTaskName", + profileId = "1", + taskId = "1", + accessCode = "1", + redeemedOn = null, + scannedOn = Clock.System.now() +) + +internal val MOCK_SYNCED_TASK_DATA_REDEEMABLE_01 = MOCK_SYNCED_TASK_DATA_01.copy( + expiresOn = Clock.System.now().plus(1.days), + acceptUntil = Clock.System.now().plus(1.days) +) + +internal val MOCK_SCANNED_TASK_DATA_REDEEMABLE_02 = MOCK_SCANNED_TASK_DATA_REDEEMABLE_01.copy( + index = 2 +) + +internal val MOCK_SCANNED_TASK_DATA_REDEEMED_01 = MOCK_SCANNED_TASK_DATA_REDEEMABLE_01.copy( + index = 3, + redeemedOn = Clock.System.now().minus(1.days) +) + +internal val MOCK_SYNCED_TASK_DATA_REDEEMABLE_02 = MOCK_SYNCED_TASK_DATA_02.copy( + expiresOn = Clock.System.now().plus(1.days), + acceptUntil = Clock.System.now().plus(1.days) +) + +internal val MEDICATION = SyncedTaskData.Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "MedicationName", + form = "AEO", + lotNumber = "1234567890", + expirationDate = FhirTemporal.Instant(Instant.DISTANT_PAST), + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "N1", + ingredientMedications = emptyList(), + manufacturingInstructions = null, + packaging = null, + ingredients = emptyList(), + amount = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ) +) + +internal val MOCK_SYNCED_TASK_DATA_REDEEMABLE_SELF_PAYER_03 = MOCK_SYNCED_TASK_DATA_02.copy( + expiresOn = Clock.System.now().plus(1.days), + acceptUntil = Clock.System.now().plus(1.days), + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = "TestInsurance", + status = "Active", + coverageType = SyncedTaskData.CoverageType.SEL + ), + medicationRequest = SyncedTaskData.MedicationRequest( + MEDICATION, null, null, SyncedTaskData.AccidentType.None, + null, null, false, null, + SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None + ) +) diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt deleted file mode 100644 index c1d5fcf2..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.fhir.model.PharmacyContacts -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class PharmacySearchViewModelTest { - @get:Rule - val coroutineRule = CoroutineTestRule() - - private lateinit var pharmacyOrderController: PharmacyOrderController - private lateinit var pharmacySearchUseCase: PharmacySearchUseCase - private lateinit var pharmacyOverviewUseCase: PharmacyOverviewUseCase - private lateinit var profileUseCase: ProfilesUseCase - private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase - private lateinit var getOrderStateUseCase: GetOrderStateUseCase - private lateinit var getShippingContactValidationUseCase: GetShippingContactValidationUseCase - - @Before - fun setUp() { - pharmacySearchUseCase = mockk() - profileUseCase = mockk() - getActiveProfileUseCase = mockk() - pharmacyOverviewUseCase = mockk() - getOrderStateUseCase = mockk() - getShippingContactValidationUseCase = mockk() - every { pharmacySearchUseCase.prescriptionDetailsForOrdering(any()) } returns flowOf(orderState) - every { getActiveProfileUseCase.invoke() } returns flowOf(activeProfile) - every { profileUseCase.profiles } returns flowOf(listOf(profile)) - every { getOrderStateUseCase.invoke() } returns flowOf(orderState) - - pharmacyOrderController = PharmacyOrderController( - getActiveProfileUseCase = getActiveProfileUseCase, - pharmacySearchUseCase = pharmacySearchUseCase, - getOrderStateUseCase = getOrderStateUseCase, - getShippingContactValidationUseCase = getShippingContactValidationUseCase, - scope = TestScope() - ) - - pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) - } - - @Test - fun `order screen state - default`() = runTest { - val result = pharmacyOrderController.updatedOrdersForTest.first() - assertEquals(contacts, result.contact) - assertEquals(prescriptions, result.orders) - } - - @Test - fun `order screen state - select prescriptions`() = runTest { - pharmacyOrderController.onSelectPrescription(prescriptions[0]) - pharmacyOrderController.onSelectPrescription(prescriptions[1]) - pharmacyOrderController.onSelectPrescription(prescriptions[2]) - - pharmacyOrderController.onDeselectPrescription(prescriptions[0]) - - val state = pharmacyOrderController.updatedOrdersForTest.first() - - assertEquals(contacts, state.contact) - assertEquals( - listOf(prescriptions[1], prescriptions[2]), - state.orders - ) - } - - @Test - fun `order screen state - set contacts`() = runTest { - coEvery { pharmacySearchUseCase.saveShippingContact(any()) } answers { Unit } - coEvery { pharmacySearchUseCase.prescriptionDetailsForOrdering("") } returns flowOf( - PharmacyUseCaseData.OrderState( - orders = prescriptions, - contact = contacts - ) - ) - - pharmacyOrderController.onSaveContact(contacts) - - coroutineRule.testDispatcher.scheduler.runCurrent() - coVerify(exactly = 1) { pharmacySearchUseCase.saveShippingContact(contacts) } - - val state = pharmacyOrderController.updatedOrdersForTest.first() - - assertEquals(contacts, state.contact) - assertEquals(prescriptions, state.orders) - } - - companion object { - private val activeProfile = ProfilesUseCaseData.Profile( - id = "test-active-profile", - name = "Active Profile User", - insurance = ProfileInsuranceInformation(), - active = true, - color = ProfilesData.ProfileColorNames.PINK, - lastAuthenticated = null, - ssoTokenScope = null, - image = null, - avatar = ProfilesData.Avatar.PersonalizedImage - ) - private val profile = ProfilesUseCaseData.Profile( - id = "test-inactive-profile", - name = "Inactive Profile User", - insurance = ProfileInsuranceInformation( - insuranceType = ProfilesUseCaseData.InsuranceType.NONE - ), - active = false, - color = ProfilesData.ProfileColorNames.SPRING_GRAY, - avatar = ProfilesData.Avatar.PersonalizedImage, - lastAuthenticated = null, - ssoTokenScope = null - ) - - private val prescriptions = listOf( - PharmacyUseCaseData.PrescriptionOrder( - taskId = "A", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false, - index = 0 - ), - PharmacyUseCaseData.PrescriptionOrder( - taskId = "B", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false, - index = 0 - - ), - PharmacyUseCaseData.PrescriptionOrder( - taskId = "C", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false, - index = 0 - - ) - ) - - private val pharmacy = PharmacyUseCaseData.Pharmacy( - id = "pharmacy", - name = "Test - Pharmacy", - address = null, - location = null, - distance = null, - contacts = PharmacyContacts( - phone = "0123456", - mail = "mail@mail.com", - url = "https://website.com", - pickUpUrl = "https://pickup.com", - deliveryUrl = "https://delivery.com", - onlineServiceUrl = "https://online.com" - ), - provides = listOf(), - openingHours = null, - telematikId = "telematik-id" - ) - - private val orderOption = PharmacyScreenData.OrderOption.PickupService - - private val contacts = PharmacyUseCaseData.ShippingContact( - name = "Beate Muster", - line1 = "Friedrichstraße 136", - line2 = "", - postalCode = "10117", - city = "Berlin", - telephoneNumber = "0123456789", - mail = "mail@mail.com", - deliveryInformation = "Bitte!" - ) - - private val orderState = PharmacyUseCaseData.OrderState( - orders = prescriptions, - contact = contacts - ) - } -} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCaseTest.kt new file mode 100644 index 00000000..cd90066c --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCaseTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_01 +import de.gematik.ti.erp.app.messages.mocks.MessageMocks.MOCK_SYNCED_TASK_DATA_02 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_ACTIVE_PROFILE +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SCANNED_TASK_DATA_REDEEMABLE_01 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SCANNED_TASK_DATA_REDEEMABLE_02 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SCANNED_TASK_DATA_REDEEMED_01 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SHIPPING_CONTACT +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SYNCED_TASK_DATA_REDEEMABLE_01 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SYNCED_TASK_DATA_REDEEMABLE_02 +import de.gematik.ti.erp.app.pharmacy.mocks.MOCK_SYNCED_TASK_DATA_REDEEMABLE_SELF_PAYER_03 +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.OrderState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.ShippingContact +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.mockk.coEvery +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class GetOrderStateUseCaseTest { + private val dispatcher = StandardTestDispatcher() + + private val profileRepository: ProfileRepository = mockk() + private val prescriptionRepository: PrescriptionRepository = mockk() + private val shippingContactRepository: ShippingContactRepository = mockk() + + @InjectMockKs + private lateinit var useCase: GetOrderStateUseCase + + @Before + fun setup() { + useCase = GetOrderStateUseCase( + profileRepository, + prescriptionRepository, + shippingContactRepository, + dispatcher + ) + coEvery { profileRepository.activeProfile() } returns flowOf(MOCK_ACTIVE_PROFILE) + } + + @Test + fun `invoke should return OrderState with 5 orders (3 synced, 2 scanned, 1 SelfPayer) and shippingContact`() { + coEvery { shippingContactRepository.shippingContact() } returns flowOf(MOCK_SHIPPING_CONTACT) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf( + listOf( + MOCK_SYNCED_TASK_DATA_REDEEMABLE_01, + MOCK_SYNCED_TASK_DATA_REDEEMABLE_02, + MOCK_SYNCED_TASK_DATA_REDEEMABLE_SELF_PAYER_03, + MOCK_SYNCED_TASK_DATA_01, + MOCK_SYNCED_TASK_DATA_02 + ) + ) + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf( + listOf( + MOCK_SCANNED_TASK_DATA_REDEEMABLE_01, + MOCK_SCANNED_TASK_DATA_REDEEMABLE_02, + MOCK_SCANNED_TASK_DATA_REDEEMED_01 + ) + ) + runTest(dispatcher) { + val orderState = useCase().first() + assert(orderState.prescriptionOrders.size == 5) + assertEquals(MOCK_SHIPPING_CONTACT.toModel(), orderState.contact) + assertEquals(1, orderState.selfPayerPrescriptionIds.size) + } + } + + @Test + fun `invoke should return OrderState with empty Orders and shippingContact`() { + coEvery { shippingContactRepository.shippingContact() } returns flowOf(null) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf( + listOf() + ) + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf( + listOf() + ) + runTest(dispatcher) { + val orderState = useCase().first() + + assertEquals( + OrderState( + prescriptionOrders = emptyList(), + selfPayerPrescriptionIds = emptyList(), + contact = ShippingContact.EmptyShippingContact + ), + orderState + ) + } + } +} diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt deleted file mode 100644 index 95d5a831..00000000 --- a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -// TODO: work in progress - replace fhir parser - -// -// package de.gematik.ti.erp.app.prescription -// -// import ca.uhn.fhir.context.FhirContext -// import de.gematik.ti.erp.app.db.entities.v1.task.OrganizationEntityV1 -// import de.gematik.ti.erp.app.prescription.repository.FhirOrganization -// import de.gematik.ti.erp.app.prescription.repository.extractResources -// import de.gematik.ti.erp.app.prescription.repository.toOrganizationEntityV1 -// import org.hl7.fhir.r4.model.Bundle -// import org.hl7.fhir.r4.model.Organization -// import org.hl7.fhir.r4.model.Property -// import java.util.zip.ZipEntry -// import java.util.zip.ZipFile -// import java.util.zip.ZipInputStream -// import kotlin.test.BeforeTest -// import kotlin.test.Test -// import kotlin.test.assertEquals -// -// private typealias FlatBundle = Map -// -// class MapperTest { -// -// @BeforeTest -// fun setUp() { -// } -// -// @Test -// fun `medication request`() { -// val parser = FhirContext.forR4().newXmlParser() -// -// ZipFile("src/test/res/KBV_1.0.2_1000_Auswahl.zip").use { zip -> -// zip.entries().asSequence().forEach { entry -> -// println(entry.name) -// zip.getInputStream(entry).use { input -> -// val bundle = try { -// parser.parseResource(input) as Bundle -// } catch (e : Exception) { -// println(e) -// null -// } -// -// if (bundle != null) { -// val flatBundle = walkBundle(bundle) -// -// flatBundle.print() -// -// testOrganizationEntityV1( -// flatBundle, -// bundle.extractResources().firstOrNull()!!.toOrganizationEntityV1() -// ) -// -// flatBundle.print() -// } -// } -// return@use -// } -// } -// } -// -// private fun testOrganizationEntityV1(flatBundle: FlatBundle, organization: OrganizationEntityV1) { -// val (organizationIndex) = flatBundle.indicesFor("resource","Organization") -// -// assertEquals(flatBundle["entry{$organizationIndex}.resource{0}.name"], organization.name) -// -// val (phoneIndex) = flatBundle.indicesFor("system", "phone", "entry{$organizationIndex}.resource{0}.telecom") -// -// val telecomPrefix = "entry{$organizationIndex}.resource{0}" -// assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.system"], "phone") -// assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.value"], organization.phone) -// // assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.system"], "fax") -// // assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.value"], organization.fax) -// assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.system"], "email") -// assertEquals(flatBundle["$telecomPrefix.telecom{$phoneIndex}.value"], organization.mail) -// // entry{5}.resource{0}.telecom{0}.value: 0301234567 -// // entry{5}.resource{0}.telecom{1}.system: fax -// // entry{5}.resource{0}.telecom{1}.value: 030123456789 -// // entry{5}.resource{0}.telecom{2}.system: email -// // entry{5}.resource{0}.telecom{2}.value: mvz@e-mail.de -// // entry{5}.resource{0}.address{0}.type: both -// // entry{5}.resource{0}.address{0}.line: Herbert-Lewin-Platz 2 -// -// } -// -// -// -// // fun List>.valueOf(path: String) = find { (p, _) -> p == path }?.second -// // -// // fun List>.pathPrefixForResource(type: String, prefix: String = ""): String? = -// // this.find { (name, value) -> -// // name.startsWith(prefix) && name.endsWith("resource") && value == type -// // }?.let { (name) -> -// // name -// // } -// -// -// // -// // fun List>.pathPrefixFor(path: String, value: String, prefix: String = ""): String? { -// // val pathRegex = path.replace("{?}", "\\{\\d}").toRegex() -// // -// // return find { (name, v) -> -// // if (name.startsWith(prefix)) { -// // name.removePrefix(prefix).matches(pathRegex) && v == value -// // } else { -// // false -// // } -// // }?.let { (name) -> -// // name -// // } -// // } -// // -// // fun String.popPath() = -// // this@popPath.substringBeforeLast('.') -// -// -// } -// -// private fun FlatBundle.print() { -// forEach { (k, v) -> -// println("$k: $v") -// } -// } -// -// private val rg = """\{(\d)}""".toRegex() -// -// private fun FlatBundle.indicesFor(type: String, value: String, prefix: String = ""): List { -// // find matching entry -// val entry = entries.find { (name, v) -> -// name.startsWith(prefix) && name.endsWith(type) && v == value -// } -// -// // return all indices contained in `{?}` -// return entry?.let { (name) -> -// rg.findAll(name.removePrefix(prefix)).map { match -> -// match.groupValues[1].toInt() -// }.toList() -// } ?: emptyList() -// } -// -// private fun walkBundle(bundle: Bundle): FlatBundle { -// val out = mutableMapOf() -// bundle.children().forEach { -// walk(property = it, name = null, out = out) -// } -// return out -// } -// -// private fun walk(property: Property, name: String?, out: MutableMap) { -// val prefix = name?.let { "$name.${property.name}" } ?: property.name -// -// property.values.forEachIndexed { index, base -> -// if (base.isPrimitive) { -// out[prefix] = base.primitiveValue() -// } -// if (base.isResource) { -// out[prefix] = base.fhirType() -// } -// base.children().forEach { -// walk(it, "$prefix{$index}", out) -// } -// } -// } diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt index 8353c610..59a56d20 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length") diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt index 8542d1a1..ecc6c250 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui.model import kotlinx.datetime.Clock -import kotlin.test.Test +import org.junit.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes @@ -104,4 +104,14 @@ class SentOrCompletedTest { assertEquals(SentOrCompletedPhrase.SentOn(lastModified), result) } + + @Test + fun `not Completed but provided`() { + val now = Clock.System.now() + val lastModified = now - 1.days + + val result = sentOrCompleted(lastModified = lastModified, now = now, completed = false, provided = true) + + assertEquals(SentOrCompletedPhrase.ProvidedOn(lastModified), result) + } } diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt index ec440b8e..f4dd8246 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt index 6ab256ac..d32ea9de 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase @@ -55,7 +55,7 @@ class ProfilesUseCaseTest { id = "1234567890", name = "Test", insurance = ProfileInsuranceInformation(), - active = false, + isActive = false, color = ProfilesData.ProfileColorNames.PINK, lastAuthenticated = null, ssoTokenScope = null, @@ -88,11 +88,4 @@ class ProfilesUseCaseTest { coVerify(exactly = 0) { profilesRepository.updateProfileName(any(), any()) } } - - @Test - fun `replace last profile`() = runTest { - assertFails { - profilesUseCase.removeAndSaveProfile(profile, "") - } - } } diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt index 42d90097..422e6169 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt index 4ff42a72..334c36ec 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt index 645d91eb..8ff5234d 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt @@ -1,23 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("UnusedPrivateMember", "MayBeConstant", "MagicNumber") + package de.gematik.ti.erp.app.utils +import de.gematik.ti.erp.app.pharmacy.mocks.MEDICATION +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import kotlinx.datetime.Clock @@ -66,29 +71,34 @@ fun syncedTask( ), insuranceInformation = SyncedTaskData.InsuranceInformation( name = null, - status = null + status = null, + coverageType = SyncedTaskData.CoverageType.GKV ), expiresOn = expiresOn, acceptUntil = acceptUntil, authoredOn = authoredOn, status = status, medicationRequest = SyncedTaskData.MedicationRequest( - medication = SyncedTaskData.MedicationPZN( + medication = SyncedTaskData.Medication( category = SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, vaccine = false, text = medicationName, form = null, lotNumber = null, expirationDate = null, - uniqueIdentifier = "", + identifier = SyncedTaskData.Identifier(), normSizeCode = null, - amount = SyncedTaskData.Ratio( - numerator = SyncedTaskData.Quantity( + amount = Ratio( + numerator = Quantity( value = "", unit = "" ), denominator = null - ) + ), + ingredientMedications = emptyList(), + manufacturingInstructions = null, + packaging = null, + ingredients = emptyList() ), dateOfAccident = null, location = null, @@ -103,7 +113,7 @@ fun syncedTask( SyncedTaskData.MedicationDispense( dispenseId = null, patientIdentifier = "", - medication = null, + medication = MEDICATION, wasSubstituted = false, dosageInstruction = "", performer = "", @@ -114,6 +124,7 @@ fun syncedTask( emptyList() }, communications = listOf(), + lastMedicationDispense = medicationDispenseWhenHandedOver?.toInstant(), failureToReport = "abcdefg" ) diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt index bf1ad4c3..17548a38 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt index 03c09cd2..e8f6e71e 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt @@ -1,29 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") +@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping", "MayBeConst", "UnusedPrivateMember") package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList import kotlinx.datetime.Instant -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okio.ByteString.Companion.decodeBase64 import org.bouncycastle.cert.X509CertificateHolder @@ -58,7 +57,7 @@ object TestCertificates { const val SerialNumber = "347632017809591" - // FIXME second ca is ocsp response only; production ocsp uses same ca as vau/idp + // TODO second ca is ocsp response only; production ocsp uses same ca as vau/idp val JsonCertList = """ { "add_roots": [], diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt index 246a53c9..f328dc7c 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau @@ -46,10 +46,10 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.bouncycastle.cert.X509CertificateHolder import org.junit.Assume +import org.junit.Before import org.junit.Rule +import org.junit.Test import retrofit2.Retrofit -import kotlin.test.BeforeTest -import kotlin.test.Test import kotlin.time.Duration @OptIn(ExperimentalCoroutinesApi::class) @@ -67,7 +67,7 @@ class TruststoreIntegrationTest { encodeDefaults = true }.asConverterFactory("application/json".toMediaType()) - @BeforeTest + @Before fun setup() { MockKAnnotations.init(this) } @@ -87,6 +87,7 @@ class TruststoreIntegrationTest { chain.proceed( chain.request().newBuilder() .addHeader("User-Agent", BuildKonfig.USER_AGENT) + .header("X-Api-Key", BuildKonfig.ERP_API_KEY) .addHeader("Accept", "application/json") .build() ) diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt index 4b82e807..d0df1f62 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length") @@ -223,7 +223,7 @@ class TruststoreTest { // use case tests - @Test + /* @Test fun `new instance of truststore contains no cached store - fetches and creates store from repository`() = runTest { coEvery { repository.withUntrusted(any()) } coAnswers { @@ -348,7 +348,7 @@ class TruststoreTest { coVerify(exactly = 1) { repository.invalidate() } verify(exactly = 2) { trustedTruststore.vauPublicKey } coVerify(exactly = 1) { trustedTruststore.checkValidity(any(), any()) } - } + }*/ @Test(expected = Exception::class) fun `truststore creation finally fails`() = @@ -390,7 +390,7 @@ class TruststoreTest { } } - @Test(expected = Exception::class) + /* @Test(expected = Exception::class) fun `truststore creation succeeds - block throws exception`() = runTest { coEvery { @@ -546,5 +546,5 @@ class TruststoreTest { verify(exactly = 0) { trustedTruststore.vauPublicKey } coVerify(exactly = 0) { trustedTruststore.checkValidity(any(), any()) } } - } + }*/ } diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt index aa16593c..512c36a3 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.repository diff --git a/app/demo-mode/build.gradle.kts b/app/demo-mode/build.gradle.kts index 92f9ea0e..e46f351c 100644 --- a/app/demo-mode/build.gradle.kts +++ b/app/demo-mode/build.gradle.kts @@ -1,131 +1,20 @@ -@file:Suppress("UnstableApiUsage") - -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import org.owasp.dependencycheck.reporting.ReportGenerator.Format +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin plugins { - id("com.android.library") - kotlin("android") - kotlin("plugin.serialization") - id("org.jetbrains.compose") - id("io.realm.kotlin") - id("kotlin-parcelize") - id("org.owasp.dependencycheck") - id("com.jaredsburrows.license") - id("de.gematik.ti.erp.dependencies") - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("de.gematik.ti.erp.technical-requirements") + id("base-android-library") + id("de.gematik.ti.erp.names") } -licenseReport { - generateCsvReport = false - generateHtmlReport = false - generateJsonReport = true - copyJsonReportToAssets = true -} +val gematik = AppDependencyNamesPlugin() android { - namespace = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.demomode" + namespace = gematik.moduleName("demomode") defaultConfig { - testApplicationId = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.demomode.test" - } - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - dependencyCheck { - analyzers.assemblyEnabled = false - suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" - formats = listOf(Format.HTML, Format.XML) - scanConfigurations = configurations.filter { - it.name.startsWith("api") || - it.name.startsWith("implementation") || - it.name.startsWith("kapt") - }.map { it.name } - } - buildTypes { - create("minifiedDebug") - } - // disable build config for demo-mode since we use only from features. - // If needed we need a new-namespace - buildFeatures { - buildConfig = false - resValues = false + testApplicationId = gematik.moduleName("demomode.test") } } dependencies { - implementation(project(":common")) - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) - inject { - coroutines { - implementation(coroutinesCore) - implementation(coroutinesAndroid) - } - dateTime { - implementation(datetime) - } - androidX { - implementation(legacySupport) - implementation(appcompat) - implementation(coreKtx) - implementation(datastorePreferences) - implementation(security) - implementation(lifecycleViewmodel) - implementation(lifecycleComposeRuntime) - implementation(lifecycleProcess) - implementation(composeNavigation) - implementation(composeActivity) - implementation(composePaging) - implementation(camerax2) - implementation(cameraxLifecycle) - implementation(cameraxView) - } - accompanist { - implementation(systemUiController) - } - dependencyInjection { - compileOnly(kodeinCompose) - } - logging { - implementation(napier) - } - lottie { - implementation(lottie) - } - serialization { - implementation(kotlinXJson) - } - crypto { - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - } - compose { - implementation(runtime) - implementation(foundation) - implementation(animation) - implementation(uiTooling) - implementation(preview) - } - material { - implementation(material) - implementation(material3) - implementation(materialIcons) - implementation(materialIconsExtended) - } - tracking { - implementation(contentSquare) - } - } -} - -secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) { - "ci-overrides.properties" - } else { - "gradle.properties" - } + implementation(project(gematik.multiplatform)) + implementation(project(gematik.uiComponents)) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt index 575826f2..7b9d10ce 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode @@ -31,6 +31,7 @@ object DemoModeIntent { ) = Intent(context, T::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK action = demoModeAction.name + `package` = context.packageName } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt index c7c6db09..e4b3b37c 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt index 787204dc..9e547b68 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity import de.gematik.ti.erp.app.demomode.datasource.data.DemoAuditEventInfo import de.gematik.ti.erp.app.demomode.datasource.data.DemoPharmacyInfo.demoFavouritePharmacy import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoScannedPrescription.demoScannedTask01 @@ -25,18 +26,23 @@ import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoS import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoSyncedPrescription.syncedTask import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile01 import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile02 +import de.gematik.ti.erp.app.demomode.datasource.data.inAppMessageEntity import de.gematik.ti.erp.app.demomode.model.DemoModeProfile import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication import de.gematik.ti.erp.app.idp.api.models.PairingData import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry -import de.gematik.ti.erp.app.orders.repository.CachedPharmacy +import de.gematik.ti.erp.app.messages.repository.CachedPharmacy import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationDispReq +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationReply import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.protocol.model.AuditEventData import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock +import java.util.UUID import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours const val INDEX_OUT_OF_BOUNDS = -1 @@ -48,27 +54,47 @@ class DemoModeDataSource { val profiles: MutableStateFlow> = MutableStateFlow(mutableListOf(demoProfile01, demoProfile02)) + private val syncedTasksList = listOf( + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 1), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 2), + syncedTask( + demoProfile01.id, + status = SyncedTaskData.TaskStatus.Ready, + index = 3, + isDirectAssignment = true + ), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Canceled, index = 4), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 5), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 6), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 7), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 8), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 9), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 10), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 11), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 12), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 13), + + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 14), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 15), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 16), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 17), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 18), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 19), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 20), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 21), + syncedTask( + demoProfile02.id, + status = SyncedTaskData.TaskStatus.Completed, + index = 22, + isDirectAssignment = true + ) + ) + /** * Data sources for the [syncedTasks] created in the demo-mode */ val syncedTasks: MutableStateFlow> = - MutableStateFlow( - mutableListOf( - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 1), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 2), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.InProgress, index = 3), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Canceled, index = 4), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 5), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 6), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 7), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 8), - - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 9), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 10), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 11), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.InProgress, index = 12) - ) - ) + MutableStateFlow(syncedTasksList.toMutableList()) /** * Data sources for the [scannedTasks] created in the demo-mode @@ -110,12 +136,27 @@ class DemoModeDataSource { ) /** - * Data sources for the [communications] created in the demo-mode, + * Data sources for the [requestCommunication] created in the demo-mode, * this is used as the source for communication between the user, pharmacy and the doctor */ val communications: MutableStateFlow> = MutableStateFlow(mutableListOf()) + val inAppMessages: MutableStateFlow> = + MutableStateFlow(mutableListOf(inAppMessageEntity)) + + val counter: MutableStateFlow = + MutableStateFlow(1) + + val lastVersion: MutableStateFlow = + MutableStateFlow("1.26.0") + + val lastUpdatedVersion: MutableStateFlow = + MutableStateFlow("1.26.0") + + val showWelcomeMessage: MutableStateFlow = + MutableStateFlow(false) + /** * Data source for the a [profileCommunicationLog] communication log that a particular profile has downloaded the information */ @@ -150,4 +191,115 @@ class DemoModeDataSource { ) ) ) + + companion object { + val communicationPayload: String = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "info_text":"Beispieltext für die Kommunikation zwischen Patient und Apotheke" + "pickUpCodeHR":"1234567890" , + "pickUpCodeDMC":"0123456789" , + "url":"https://www.gematik.de/" + } + """.trimIndent() + + fun replyCommunications( + profileId: String, + taskId: String, + communicationId: String, + pharmacyId: String, + orderId: String, + consumed: Boolean = false + ) = listOf( + // T-01 + DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = taskId, + communicationId = communicationId, + sentOn = Clock.System.now().minus(3.days).minus(2.hours), + sender = pharmacyId, + consumed = consumed, + profile = ErxCommunicationReply, + // these values are kept empty while saving them + orderId = orderId, + payload = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "info_text":"Eine Beispielnachricht, wie eine Nachricht aus der Apotheke aussieht" , + "pickUpCodeHR":"T01" , + "pickUpCodeDMC":"DMC01" , + "url":"https://github.com/gematik/E-Rezept-App-Android" + } + """.trimIndent(), + recipient = "Erika Mustermann" + ), + // T-02 + DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = taskId, + communicationId = communicationId, + sentOn = Clock.System.now().minus(4.days).minus(3.hours), + sender = pharmacyId, + consumed = consumed, + profile = ErxCommunicationReply, + // these values are kept empty while saving them + orderId = orderId, + payload = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "info_text":"" , + "pickUpCodeHR":"", + "pickUpCodeDMC":"" + } + """.trimIndent(), + recipient = "Max Mustermann" + ), + // T-03 + DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = taskId, + communicationId = communicationId, + sentOn = Clock.System.now().minus(5.days), + sender = pharmacyId, + consumed = consumed, + profile = ErxCommunicationReply, + orderId = orderId, + payload = """ + { + "version":1 , + "supplyOptionsType":"onPremise" , + "pickUpCodeHR":"T03", + "pickUpCodeDMC":"" + } + """.trimIndent(), + recipient = "Mustermann" + ) + ) + + fun requestCommunication( + profileId: String, + taskId: String, + communicationId: String, + pharmacyId: String, + consumed: Boolean = false + ): DemoModeProfileLinkedCommunication { + val orderId = UUID.randomUUID().toString() + return DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = taskId, + communicationId = communicationId, + sentOn = Clock.System.now().minus(2.days).minus(1.hours), + sender = pharmacyId, + consumed = consumed, + profile = ErxCommunicationDispReq, + // these values are kept empty while saving them + orderId = orderId, + payload = "", + recipient = "Max Mustermann" + ) + } + } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt index 793da09e..03b6967e 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource.data diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConsentInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConsentInfo.kt index 9be3337c..4c89e692 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConsentInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConsentInfo.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource.data diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt index 84520423..b50d0930 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource.data @@ -27,8 +27,9 @@ object DemoConstants { internal val longerRandomTimeToday = Clock.System.now().minus((2..58).random().minutes) internal const val PHARMACY_TELEMATIK_ID = "3-03.2.1006210000.10.795" internal const val SYNCED_TASK_PRESET = "110.000.002.345.863" + internal const val DIRECT_ASSIGNMENT_TASK_PRESET = "169.000.002.345.863" internal val NOW = Clock.System.now() internal val START_DATE = Clock.System.now().minus(5.minutes) internal val EXPIRY_DATE = Clock.System.now().plus(200.days) - internal val SHORT_EXPIRY_DATE = Clock.System.now().plus(20.days) + internal val SHORT_EXPIRY_DATE = Clock.System.now().plus(30.days) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoInAppMessageEntityDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoInAppMessageEntityDataSource.kt new file mode 100644 index 00000000..6ac06e0d --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoInAppMessageEntityDataSource.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity + +val inAppMessageEntity = InAppMessageEntity().apply { + id = "01" + this.version = "1.27.1" +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt index 2811d773..1d0e370d 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource.data diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt index 48608340..58096ed2 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.datasource.data +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.DIRECT_ASSIGNMENT_TASK_PRESET import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.EXPIRY_DATE import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.NOW import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.SHORT_EXPIRY_DATE @@ -25,16 +26,16 @@ import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.SYNCED_TASK_ import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.longerRandomTimeToday import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.randomTimeToday import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile01 +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationDispense -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationPZN +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Medication import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Quantity -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Ratio import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.utils.FhirTemporal import java.util.UUID @@ -42,9 +43,13 @@ import kotlin.random.Random object DemoPrescriptionInfo { - private val SYNCED_MEDICATION_NAMES = listOf( + private val BOOLEAN = listOf(true, false) + + private val SYNCED_MEDICATION_NAMES = setOf( "Ibuprofen 600", "Meloxicam", "Indomethacin", "Celebrex", "Ketoprofen", "Piroxicam", "Etodolac", "Toradol", - "Aspirin", "Voltaren" + "Aspirin", "Voltaren", "Naproxen", "Mobic", "Aleve", "Motrin", "Advil", "Relafen", "Feldene", "Daypro", + "Clinoril", "Ansaid", "Orudis", "Dolobid", "Tolectin", "Lodine", "Nalfon", "Indocin", "Arthrotec", "Vimovo", + "Cataflam", "Pennsaid", "Zipsor", "Voltaren Gel" ) val SCANNED_MEDICINE_NAMES = listOf( @@ -242,37 +247,66 @@ object DemoPrescriptionInfo { denominator = null ) - private val MEDICATION = MedicationPZN( - category = SyncedTaskData.MedicationCategory.values().random(), + private val MEDICATION = Medication( + category = SyncedTaskData.MedicationCategory.entries.toTypedArray().random(), vaccine = Random.nextBoolean(), text = SYNCED_MEDICATION_NAMES.random(), form = codeToFormMapping.random(), lotNumber = DEMO_MODE_IDENTIFIER, expirationDate = FhirTemporal.Instant(EXPIRY_DATE), - uniqueIdentifier = DEMO_MODE_IDENTIFIER, + identifier = SyncedTaskData.Identifier(DEMO_MODE_IDENTIFIER), normSizeCode = normSizeMappings.random(), - amount = RATIO + amount = RATIO, + ingredientMedications = emptyList(), + ingredients = emptyList(), + manufacturingInstructions = null, + packaging = null + ) + + private val COVERAGE_TYPE = SyncedTaskData.CoverageType.entries + .toTypedArray().random() + + private fun medication(index: Int) = Medication( + category = SyncedTaskData.MedicationCategory.entries.toTypedArray().random(), + vaccine = Random.nextBoolean(), + text = SYNCED_MEDICATION_NAMES.elementAtOrElse(index) { SYNCED_MEDICATION_NAMES.random() }, + form = codeToFormMapping.random(), + lotNumber = DEMO_MODE_IDENTIFIER, + expirationDate = FhirTemporal.Instant(EXPIRY_DATE), + identifier = SyncedTaskData.Identifier(DEMO_MODE_IDENTIFIER), + normSizeCode = normSizeMappings.random(), + amount = RATIO, + ingredientMedications = emptyList(), + ingredients = emptyList(), + manufacturingInstructions = null, + packaging = null ) internal val MEDICATION_DISPENSE = MedicationDispense( dispenseId = UUID.randomUUID().toString(), patientIdentifier = PATIENT.insuranceIdentifier ?: "", medication = MEDICATION, - wasSubstituted = false, + wasSubstituted = BOOLEAN.random(), dosageInstruction = DOSAGE.random(), performer = PERFORMERS.random(), whenHandedOver = null ) - internal val MEDICATION_REQUEST = MedicationRequest( - medication = MEDICATION, + internal val INSURANCE_INFORMATION = SyncedTaskData.InsuranceInformation( + name = null, + status = null, + coverageType = COVERAGE_TYPE + ) + + internal fun medicationRequest(index: Int) = MedicationRequest( + medication = medication(index), dateOfAccident = null, location = CITY_NAMES.random(), - emergencyFee = Random.nextBoolean(), + emergencyFee = BOOLEAN.random(), dosageInstruction = DOSAGE.random(), multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), note = DOCTORS_NOTES.random(), - substitutionAllowed = Random.nextBoolean() + substitutionAllowed = BOOLEAN.random() ) internal object DemoScannedPrescription { @@ -302,29 +336,32 @@ object DemoPrescriptionInfo { internal fun syncedTask( profileIdentifier: ProfileIdentifier, status: SyncedTaskData.TaskStatus = SyncedTaskData.TaskStatus.Ready, + isDirectAssignment: Boolean = false, index: Int - ) = SyncedTaskData.SyncedTask( - profileId = profileIdentifier, - taskId = "$SYNCED_TASK_PRESET.$index", - isIncomplete = false, - pvsIdentifier = DEMO_MODE_IDENTIFIER, - accessCode = DEMO_MODE_IDENTIFIER, - lastModified = longerRandomTimeToday, - organization = ORGANIZATION, - practitioner = PRACTITIONER, - patient = PATIENT, - insuranceInformation = SyncedTaskData.InsuranceInformation( - name = null, - status = null - ), - expiresOn = EXPIRY_DATE, - acceptUntil = SHORT_EXPIRY_DATE, - authoredOn = NOW, - status = status, - medicationRequest = MEDICATION_REQUEST, - medicationDispenses = listOf(MEDICATION_DISPENSE), - communications = emptyList(), - failureToReport = "" - ) + ): SyncedTaskData.SyncedTask { + val taskId = + if (isDirectAssignment) "$DIRECT_ASSIGNMENT_TASK_PRESET.$index" else "$SYNCED_TASK_PRESET.$index" + return SyncedTaskData.SyncedTask( + profileId = profileIdentifier, + taskId = taskId, + isIncomplete = false, // making this true makes the prescription defective + pvsIdentifier = DEMO_MODE_IDENTIFIER, + accessCode = DEMO_MODE_IDENTIFIER, + lastModified = longerRandomTimeToday, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFORMATION, + expiresOn = EXPIRY_DATE, + acceptUntil = SHORT_EXPIRY_DATE, + authoredOn = NOW, + status = status, + medicationRequest = medicationRequest(index), + lastMedicationDispense = null, + medicationDispenses = listOf(MEDICATION_DISPENSE), + communications = emptyList(), + failureToReport = "" + ) + } } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt index e95773ae..cbc06687 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt @@ -1,40 +1,35 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MagicNumber") package de.gematik.ti.erp.app.demomode.datasource.data -import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.EXPIRY_DATE import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.START_DATE import de.gematik.ti.erp.app.demomode.model.DemoModeProfile import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.model.ProfilesData import kotlinx.datetime.Instant -import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.util.encoders.Base64 import java.util.UUID object DemoProfileInfo { - private const val CAN = "123123" - private val byteArray = Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) - private val HEALTH_CERTIFICATE = X509CertificateHolder(byteArray) + private const val AUTHENTICATOR_NAME = "Gematik Versicherung" private val singleSignOnToken = IdpData.SingleSignOnToken( token = UUID.randomUUID().toString(), expiresOn = EXPIRY_DATE, @@ -42,10 +37,10 @@ object DemoProfileInfo { ) // TODO: Add demo mode for different modes of sign-on scopes - private val cardToken = IdpData.DefaultToken( + private val token = IdpData.ExternalAuthenticationToken( token = singleSignOnToken, - cardAccessNumber = CAN, - healthCardCertificate = HEALTH_CERTIFICATE + authenticatorName = AUTHENTICATOR_NAME, + authenticatorId = UUID.randomUUID().toString() ) private val HEALTH_INSURANCE_COMPANIES = listOf( "GesundheitsVersichert AG", @@ -70,7 +65,7 @@ object DemoProfileInfo { profileName = "Erika Mustermann", isActive = true, color = ProfilesData.ProfileColorNames.SUN_DEW, - insuranceType = ProfilesData.InsuranceType.PKV, // Note: Private insurance account + insuranceType = ProfilesData.InsuranceType.GKV, avatar = listOf( ProfilesData.Avatar.FemaleDoctor, ProfilesData.Avatar.FemaleDoctorWithPhone, @@ -99,14 +94,18 @@ object DemoProfileInfo { lastAuthenticated = null ) + internal fun demoEmptyProfile(name: String) = profile( + name + ) + private fun profile( profileName: String, isActive: Boolean = true, - color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.values().random(), - avatar: ProfilesData.Avatar = ProfilesData.Avatar.values().random(), + color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.entries.toTypedArray().random(), + avatar: ProfilesData.Avatar = ProfilesData.Avatar.entries.toTypedArray().random(), insuranceType: ProfilesData.InsuranceType = ProfilesData.InsuranceType.GKV, lastAuthenticated: Instant? = null, - singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? = cardToken + singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? = token ): DemoModeProfile { val uuid = UUID.randomUUID() return DemoModeProfile( @@ -125,5 +124,5 @@ object DemoProfileInfo { ) } - internal fun String.create() = profile(profileName = this) + internal fun String.create(): DemoModeProfile = profile(profileName = this) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt index 93c90dd9..70b72f14 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt @@ -1,43 +1,51 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.di import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository import de.gematik.ti.erp.app.consent.repository.ConsentRepository import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource import de.gematik.ti.erp.app.demomode.mapper.authentication.DemoPromptAuthenticationProvider import de.gematik.ti.erp.app.demomode.repository.consent.DemoConsentRepository import de.gematik.ti.erp.app.demomode.repository.orders.DemoCommunicationRepository import de.gematik.ti.erp.app.demomode.repository.orders.DemoDownloadCommunicationResource -import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoPharmacyLocalDataSource +import de.gematik.ti.erp.app.demomode.repository.orders.DemoInAppMessageRepository +import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoFavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoOftenUsePharmacyLocalDataSource +import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoRedeemLocalDataSource +import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoShippingContactRepository import de.gematik.ti.erp.app.demomode.repository.prescriptions.DemoPrescriptionsRepository import de.gematik.ti.erp.app.demomode.repository.prescriptions.DemoTaskRepository import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository import de.gematik.ti.erp.app.demomode.repository.protocol.DemoAuditEventsRepository import de.gematik.ti.erp.app.demomode.usecase.idp.DemoIdpUseCase import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository import de.gematik.ti.erp.app.prescription.repository.TaskRepository import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.bindSingleton @@ -49,15 +57,21 @@ val demoModeModule = DI.Module("demoModeModule") { bindSingleton { DemoModeDataSource() } } +// TODO DemoMode fun DI.MainBuilder.demoModeOverrides() { bindProvider(overrides = true) { DemoProfilesRepository(instance()) } bindProvider(overrides = true) { DemoConsentRepository() } bindProvider(overrides = true) { DemoPrescriptionsRepository(instance()) } bindProvider(overrides = true) { DemoAuditEventsRepository(instance()) } - bindProvider(overrides = true) { DemoPharmacyLocalDataSource(instance()) } + bindProvider(overrides = true) { DemoRedeemLocalDataSource(instance()) } + bindProvider(overrides = true) { DemoFavouritePharmacyLocalDataSource(instance()) } + bindProvider(overrides = true) { DemoOftenUsePharmacyLocalDataSource(instance()) } bindProvider(overrides = true) { DemoCommunicationRepository(instance(), instance()) } + bindProvider(overrides = true) { DemoInAppMessageRepository(instance()) } bindProvider(overrides = true) { DemoTaskRepository() } bindProvider(overrides = true) { DemoIdpUseCase(instance()) } + bindProvider(overrides = true) { DemoShippingContactRepository() } + // these two are added for future functions bindProvider(overrides = true) { DemoPromptAuthenticationProvider() } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/extensions/NapierExtensions.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/extensions/NapierExtensions.kt new file mode 100644 index 00000000..24cdebb5 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/extensions/NapierExtensions.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.extensions + +import io.github.aakira.napier.Napier + +internal fun Napier.demo(throwable: Throwable? = null, message: () -> String) = i(throwable, "Demo-mode", message) diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt index 4f4fed66..960fc994 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.mapper.authentication diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt index 4dfb8e9c..e6657da4 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.model @@ -48,7 +48,7 @@ fun DemoModeProfile.toProfile() = ProfilesData.Profile( id = id, color = color, avatar = avatar, - personalizedImage = personalizedImage, + image = personalizedImage, name = name, insurantName = insurantName, insuranceIdentifier = insuranceIdentifier, diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt index 8189c541..d4fed8a6 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.model @@ -24,6 +24,7 @@ import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.UUID +import kotlin.time.Duration.Companion.minutes /** * This is created to link the communication with a particular profile @@ -54,6 +55,7 @@ fun DemoModeProfileLinkedCommunication.toSyncedTaskDataCommunication() = consumed = consumed ) +@Suppress("MagicNumber") internal fun DemoModeSentCommunicationJson.toDemoModeProfileLinkedCommunication( profileId: ProfileIdentifier ) = @@ -66,7 +68,7 @@ internal fun DemoModeSentCommunicationJson.toDemoModeProfileLinkedCommunication( true -> CommunicationProfile.ErxCommunicationDispReq false -> CommunicationProfile.ErxCommunicationReply }, - sentOn = Clock.System.now(), + sentOn = Clock.System.now().minus((1..20).random().minutes), sender = payload.firstNotNullOfOrNull { it.name } ?: "", recipient = recipient.firstNotNullOfOrNull { it.identifier.value } ?: "", payload = payload.firstNotNullOfOrNull { it.contentString }, diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt index c24b3ec5..f5ef60e7 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.model diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/consent/DemoConsentRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/consent/DemoConsentRepository.kt index aad18896..660f2f74 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/consent/DemoConsentRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/consent/DemoConsentRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.repository.consent @@ -45,4 +45,6 @@ class DemoConsentRepository : ConsentRepository { override fun isConsentDrawerShown(profileId: ProfileIdentifier): Boolean = true override fun isConsentGranted(it: JsonElement): Boolean = true + + override fun getInsuranceId(profileId: ProfileIdentifier): String = "X1234567890" } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt index 0311b6a1..2815b72e 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("TooManyFunctions", "MagicNumber") @@ -26,26 +26,32 @@ import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.longerRandomTimeToday import de.gematik.ti.erp.app.demomode.datasource.data.DemoPharmacyInfo.PHARMACY_NAMES import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo +import de.gematik.ti.erp.app.demomode.extensions.demo import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication +import de.gematik.ti.erp.app.demomode.model.toProfile import de.gematik.ti.erp.app.demomode.model.toSyncedTaskDataCommunication -import de.gematik.ti.erp.app.orders.repository.CachedPharmacy -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.messages.repository.CachedPharmacy +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository import de.gematik.ti.erp.app.prescription.model.Communication import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationDispReq import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationReply import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -53,7 +59,10 @@ import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import java.util.UUID import kotlin.random.Random +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes class DemoCommunicationRepository( private val dataSource: DemoModeDataSource, @@ -62,9 +71,68 @@ class DemoCommunicationRepository( ) : CommunicationRepository { private val scope = CoroutineScope(dispatcher) - override val pharmacyCacheError = MutableSharedFlow() + override val pharmacyCacheError = Channel() + override val pharmacyDownloaded = Channel() override suspend fun downloadCommunications(profileId: ProfileIdentifier) = withContext(dispatcher) { + Napier.demo { "Simulating communication download" } + dataSource.communications.value = dataSource.communications.updateAndGet { communications -> + val taskIds = dataSource.syncedTasks.first() + .filter { it.profileId == profileId } + .filter { it.status == SyncedTaskData.TaskStatus.InProgress } + .map { it.taskId } + communications.all { it.taskId in taskIds } + + taskIds.map { taskId -> + when { + // if the task is not already in the list + communications.none { it.taskId == taskId && it.profile == ErxCommunicationReply } -> { + communications.add( + DemoModeDataSource.replyCommunications( + profileId = profileId, + taskId = taskId, + communicationId = UUID.randomUUID().toString(), + orderId = UUID.randomUUID().toString(), + pharmacyId = UUID.randomUUID().toString() + )[0] + ) + } + + communications.filter { it.taskId == taskId && it.profile == ErxCommunicationReply }.size == 1 -> { + val firstCommunication = + communications.first { it.taskId == taskId && it.profile == ErxCommunicationReply } + + communications.add( + DemoModeDataSource.replyCommunications( + profileId = profileId, + taskId = taskId, + communicationId = UUID.randomUUID().toString(), + orderId = firstCommunication.orderId, + pharmacyId = firstCommunication.sender + )[1] + ) + } + + communications.filter { it.taskId == taskId && it.profile == ErxCommunicationReply }.size == 2 -> { + val firstCommunication = + communications.first { it.taskId == taskId && it.profile == ErxCommunicationReply } + + communications.add( + DemoModeDataSource.replyCommunications( + profileId = profileId, + taskId = taskId, + communicationId = UUID.randomUUID().toString(), + orderId = firstCommunication.orderId, + pharmacyId = firstCommunication.sender + )[2] + ) + } + + else -> communications + } + } + communications + } delay(1000) // simulates a network delay of one second Result.success(Unit) } @@ -91,86 +159,111 @@ class DemoCommunicationRepository( override fun loadPharmacies(): Flow> = dataSource.cachedPharmacies - override suspend fun downloadMissingPharmacy(telematikId: String) { + override suspend fun downloadMissingPharmacy(telematikId: String): Result { + val addedPharmacy = CachedPharmacy( + telematikId = telematikId, + name = PHARMACY_NAMES.random() + ) withContext(dispatcher) { dataSource.cachedPharmacies.value = dataSource.cachedPharmacies.updateAndGet { cachedPharmacies -> - cachedPharmacies.add( - CachedPharmacy( - telematikId = telematikId, - name = PHARMACY_NAMES.random() - ) - ) + cachedPharmacies.add(addedPharmacy) cachedPharmacies } } + return Result.success(addedPharmacy) } override fun loadSyncedByTaskId(taskId: String): Flow = - dataSource.syncedTasks.map { syncedTasks -> - syncedTasks.find { it.taskId == taskId } - }.flowOn(dispatcher) + try { + dataSource.syncedTasks.map { syncedTasks -> + syncedTasks.find { it.taskId == taskId } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(null) + } override fun loadScannedByTaskId(taskId: String): Flow = - dataSource.scannedTasks.map { scannedTask -> - scannedTask.find { it.taskId == taskId } - }.flowOn(dispatcher) - - override fun loadDispReqCommunications(orderId: String) = - dataSource.communications.mapNotNull { communications -> - communications - .filter { it.orderId == orderId && it.profile == ErxCommunicationDispReq } - .map { it.toSyncedTaskDataCommunication() } - }.flowOn(dispatcher) + try { + dataSource.scannedTasks.map { scannedTask -> + scannedTask.find { it.taskId == taskId } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(null) + } - override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> { - return loadOrdersByProfileId(profileId).mapNotNull { communications -> - communications.asSequence().filter { - it.profileId == profileId && it.profile == ErxCommunicationDispReq - } - .map { it.toSyncedTaskDataCommunication() } - .sortedByDescending { it.sentOn } - .distinctBy { it.orderId } - .toList() - }.flowOn(dispatcher) - } + override fun loadDispReqCommunications(orderId: String): Flow> = + try { + dataSource.communications.mapNotNull { communications -> + communications + .also { Napier.demo { "LoadDispReqCommunications ${it.size}" } } + .filter { it.orderId == orderId && it.profile == ErxCommunicationDispReq } + .map { it.toSyncedTaskDataCommunication() } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(emptyList()) + } - override fun loadRepliedCommunications(taskIds: List) = - dataSource.communications.mapNotNull { communications -> - taskIds.mapNotNull { taskId -> - communications.find { it.taskId == taskId && it.profile == ErxCommunicationReply } - }.sortedByDescending { it.sentOn } - .map { it.toSyncedTaskDataCommunication() } - }.flowOn(dispatcher) + override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> = + try { + loadOrdersByProfileId(profileId).mapNotNull { communications -> + communications.asSequence().filter { + it.profileId == profileId && it.profile == ErxCommunicationDispReq + } + .map { it.toSyncedTaskDataCommunication() } + .sortedByDescending { it.sentOn } + .distinctBy { it.orderId } + .toList() + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(emptyList()) + } - override fun loadCommunicationsWithTaskId(taskIds: List): Flow> = - dataSource.communications.mapNotNull { communications -> - taskIds.mapNotNull { taskId -> - communications.find { it.taskId == taskId } - }.map { it.toSyncedTaskDataCommunication() } + override fun loadRepliedCommunications(taskIds: List, telematikId: String): Flow> = + try { + dataSource.communications + .mapNotNull { communications -> + communications + .filter { it.taskId in taskIds && it.profile == ErxCommunicationReply } + .sortedByDescending { it.sentOn } + .map { it.toSyncedTaskDataCommunication() } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(emptyList()) } - override fun hasUnreadPrescription(taskIds: List, orderId: String): Flow = - dataSource.communications.mapNotNull { communications -> - val booleans = taskIds.map { taskId -> - communications.find { it.taskId == taskId && !it.consumed }?.consumed == false - } - booleans.any { it } - }.flowOn(dispatcher) + override fun hasUnreadDispenseMessage(taskIds: List, orderId: String): Flow = + try { + dataSource.communications.mapNotNull { communications -> + val booleans = taskIds.map { taskId -> + communications.find { it.taskId == taskId && !it.consumed }?.consumed == false + } + booleans.any { it } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(false) + } - override fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow = - dataSource.communications.mapNotNull { communications -> - communications.any { it.profileId == profileId && !it.consumed } - }.flowOn(dispatcher) + override fun hasUnreadDispenseMessage(profileId: ProfileIdentifier): Flow = + try { + dataSource.communications.mapNotNull { communications -> + communications.any { it.profileId == profileId && !it.consumed } + }.flowOn(dispatcher) + } catch (e: Throwable) { + flowOf(false) + } - override fun unreadOrders(profileId: ProfileIdentifier): Flow = - dataSource.communications.mapNotNull { communications -> - communications.filter { - it.profileId == profileId && + override fun unreadMessagesCount(consumed: Boolean): Flow = + try { + dataSource.communications.mapNotNull { communications -> + communications.filter { !it.consumed && - it.profile == ErxCommunicationDispReq + it.profile == ErxCommunicationDispReq + } + .distinctBy { it.orderId } + .size.toLong() } - .distinctBy { it.orderId } - .size.toLong() + } catch (e: Throwable) { + flowOf(0L) } override fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = @@ -184,6 +277,16 @@ class DemoCommunicationRepository( .map { it.taskId } }.flowOn(dispatcher) + @OptIn(ExperimentalCoroutinesApi::class) + override fun profileByOrderId(orderId: String): Flow = + dataSource.communications.mapNotNull { communications -> + communications.find { communication -> communication.orderId == orderId } + }.flatMapLatest { communication -> + dataSource.profiles.mapNotNull { profiles -> + profiles.find { it.id == communication.profileId }?.toProfile() + }.flowOn(dispatcher) + } + override suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { withContext(dispatcher) { dataSource.communications.value = scope.async { @@ -214,35 +317,53 @@ class DemoCommunicationRepository( .mapNotNull { scannedTasks -> scannedTasks.find { it.taskId == taskId } }.first() - val communicationForTask = DemoModeProfileLinkedCommunication( - profileId = task.profileId, - taskId = taskId, - communicationId = transactionId, - sentOn = Clock.System.now(), - sender = pharmacyId, - consumed = false, - profile = ErxCommunicationDispReq, - // these values are kept empty while saving them - orderId = "", - payload = "", - recipient = "" + val requestMessage = task.makeRequestCommunication(transactionId, pharmacyId) + val replyCommunication = requestMessage.copy( + communicationId = UUID.randomUUID().toString(), + sentOn = Clock.System.now().plus(1.hours), + payload = DemoModeDataSource.communicationPayload, + profile = ErxCommunicationDispReq ) dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { scannedTasks -> val index = scannedTasks.indexOfFirst { it.taskId == taskId }.takeIf { it != INDEX_OUT_OF_BOUNDS } index?.let { nonNullIndex -> scannedTasks[nonNullIndex] = scannedTasks[nonNullIndex].copy( - communications = emptyList() + communications = listOf() ) } scannedTasks } dataSource.communications.value = dataSource.communications.updateAndGet { communications -> - communications.add(communicationForTask) + communications.add(requestMessage) + communications.add(replyCommunication) communications } } } + override suspend fun hasUnreadRepliedMessages(taskIds: List, telematikId: String): Flow { + return flowOf(false) + } + + private fun ScannedTaskData.ScannedTask.makeRequestCommunication( + id: String, + pharmacyId: String, + consumed: Boolean = false + ): DemoModeProfileLinkedCommunication = + DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = taskId, + communicationId = id, + sentOn = Clock.System.now().minus(1.minutes), + sender = pharmacyId, + consumed = consumed, + profile = ErxCommunicationDispReq, + // these values are kept empty while saving them + orderId = UUID.randomUUID().toString(), + payload = "", + recipient = "Mustermann" + ) + @OptIn(ExperimentalCoroutinesApi::class) private fun loadOrdersForActiveProfile() = findActiveProfile().flatMapLatest { loadOrdersByProfileId(it.id) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt index 88fe20b7..ca0340c8 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.repository.orders @@ -48,6 +48,7 @@ class DemoDownloadCommunicationResource( private val dataSource: DemoModeDataSource, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { + @Suppress("CyclomaticComplexMethod") operator fun invoke( profileId: ProfileIdentifier ): Flow> = @@ -69,7 +70,7 @@ class DemoDownloadCommunicationResource( .copy( profileId = profileId, recipient = "recipient", - payload = "payload", + payload = DemoModeDataSource.communicationPayload, consumed = false, sender = profileId, sentOn = Clock.System.now().minus(45.minutes), @@ -88,7 +89,7 @@ class DemoDownloadCommunicationResource( orderId = UUID.randomUUID().toString(), consumed = false, recipient = "recipient", - payload = "payload", + payload = DemoModeDataSource.communicationPayload, sender = "sender", sentOn = Clock.System.now().minus(3.days), profile = when (isCommunicationRequest) { diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoInAppMessageRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoInAppMessageRepository.kt new file mode 100644 index 00000000..f8351160 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoInAppMessageRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("TooManyFunctions", "MagicNumber") + +package de.gematik.ti.erp.app.demomode.repository.orders + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import io.realm.kotlin.types.RealmList +import kotlinx.coroutines.flow.Flow + +class DemoInAppMessageRepository( + private val demoModeDataSource: DemoModeDataSource +) : InAppMessageRepository { + override val inAppMessages: Flow> + get() = demoModeDataSource.inAppMessages + + override val counter: Flow + get() = demoModeDataSource.counter + + override val lastVersion: Flow + get() = demoModeDataSource.lastVersion + + override val lastUpdatedVersion: Flow + get() = demoModeDataSource.lastUpdatedVersion + + override val showWelcomeMessage: Flow + get() = demoModeDataSource.showWelcomeMessage + + override suspend fun setInternalMessageAsRead() { + // No-op + } + + override suspend fun setShowWelcomeMessage() { + // No-op + } + + override suspend fun updateChangeLogs(newChangeLogs: RealmList, lastVersion: String, inAppLastVersion: String) { + // No-op + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoFavouritePharmacyLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoFavouritePharmacyLocalDataSource.kt new file mode 100644 index 00000000..6d291f17 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoFavouritePharmacyLocalDataSource.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.repository.pharmacy + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock + +class DemoFavouritePharmacyLocalDataSource( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : FavouritePharmacyLocalDataSource { + + override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == favoritePharmacy.telematikId } + pharmacies + } + } + } + + override fun loadFavoritePharmacies(): Flow> = + dataSource.favoritePharmacies + + override suspend fun markPharmacyAsFavourite(pharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val favoritePharmacies = it.toMutableList() + favoritePharmacies + .indexOfFirst { existingPharmacy -> existingPharmacy.telematikId == pharmacy.telematikId } + .takeIf { index -> index != INDEX_OUT_OF_BOUNDS }?.let { index -> + favoritePharmacies[index] = favoritePharmacies[index].copy(lastUsed = Clock.System.now()) + favoritePharmacies + } ?: run { + val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( + lastUsed = Clock.System.now(), + usageCount = 1, + isFavorite = true, + telematikId = pharmacy.telematikId, + pharmacyName = pharmacy.name, + address = pharmacy.address ?: "---" + ) + favoritePharmacies.add(overviewPharmacy) + favoritePharmacies + } + } + } + } + + override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = + dataSource.favoritePharmacies.mapNotNull { + val favoritePharmacy = it.find { it.telematikId == pharmacy.telematikId } + favoritePharmacy != null + }.flowOn(dispatcher) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoOftenUsePharmacyLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoOftenUsePharmacyLocalDataSource.kt new file mode 100644 index 00000000..9e81146c --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoOftenUsePharmacyLocalDataSource.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.repository.pharmacy + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock + +class DemoOftenUsePharmacyLocalDataSource( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : OftenUsedPharmacyLocalDataSource { + override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { + withContext(dispatcher) { + dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } + pharmacies + } + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } + pharmacies + } + } + } + + override fun loadOftenUsedPharmacies(): Flow> = + dataSource.oftenUsedPharmacies + + override suspend fun markPharmacyAsOftenUsed(pharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { + val oftenUsedPharmacies = it.toMutableList() + oftenUsedPharmacies.indexOfFirst { item -> item.telematikId == pharmacy.telematikId } + .takeIf { index -> index != INDEX_OUT_OF_BOUNDS } + ?.let { index -> + oftenUsedPharmacies[index] = oftenUsedPharmacies[index].copy( + lastUsed = Clock.System.now(), + usageCount = oftenUsedPharmacies[index].usageCount + 1 + ) + oftenUsedPharmacies + } ?: run { + val isFavourite = dataSource.favoritePharmacies.value + .find { item -> item.telematikId == pharmacy.telematikId } != null + val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( + lastUsed = Clock.System.now(), + usageCount = 1, + isFavorite = isFavourite, + telematikId = pharmacy.telematikId, + pharmacyName = pharmacy.name, + address = pharmacy.address ?: "---" + ) + oftenUsedPharmacies.add(overviewPharmacy) + oftenUsedPharmacies + } + } + } + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt deleted file mode 100644 index 98d689bf..00000000 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.demomode.repository.pharmacy - -import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource -import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.updateAndGet -import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock - -class DemoPharmacyLocalDataSource( - private val dataSource: DemoModeDataSource, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) : PharmacyLocalDataSource { - override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - withContext(dispatcher) { - dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { - val pharmacies = it.toMutableList() - pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } - pharmacies - } - dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { - val pharmacies = it.toMutableList() - pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } - pharmacies - } - } - } - - override fun loadOftenUsedPharmacies(): Flow> = - dataSource.oftenUsedPharmacies - - override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatcher) { - dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { - val oftenUsedPharmacies = it.toMutableList() - oftenUsedPharmacies.indexOfFirst { item -> item.telematikId == pharmacy.telematikId } - .takeIf { index -> index != INDEX_OUT_OF_BOUNDS } - ?.let { index -> - oftenUsedPharmacies[index] = oftenUsedPharmacies[index].copy( - lastUsed = Clock.System.now(), - usageCount = oftenUsedPharmacies[index].usageCount + 1 - ) - oftenUsedPharmacies - } ?: run { - val isFavourite = dataSource.favoritePharmacies.value - .find { item -> item.telematikId == pharmacy.telematikId } != null - val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( - lastUsed = Clock.System.now(), - usageCount = 1, - isFavorite = isFavourite, - telematikId = pharmacy.telematikId, - pharmacyName = pharmacy.name, - address = pharmacy.address ?: "---" - ) - oftenUsedPharmacies.add(overviewPharmacy) - oftenUsedPharmacies - } - } - } - } - - override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatcher) { - dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { - val pharmacies = it.toMutableList() - pharmacies.removeIf { item -> item.telematikId == favoritePharmacy.telematikId } - pharmacies - } - } - } - - override fun loadFavoritePharmacies(): Flow> = - dataSource.favoritePharmacies - - override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatcher) { - dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { - val favoritePharmacies = it.toMutableList() - favoritePharmacies - .indexOfFirst { existingPharmacy -> existingPharmacy.telematikId == pharmacy.telematikId } - .takeIf { index -> index != INDEX_OUT_OF_BOUNDS }?.let { index -> - favoritePharmacies[index] = favoritePharmacies[index].copy(lastUsed = Clock.System.now()) - favoritePharmacies - } ?: run { - val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( - lastUsed = Clock.System.now(), - usageCount = 1, - isFavorite = true, - telematikId = pharmacy.telematikId, - pharmacyName = pharmacy.name, - address = pharmacy.address ?: "---" - ) - favoritePharmacies.add(overviewPharmacy) - favoritePharmacies - } - } - } - } - - override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = - dataSource.favoritePharmacies.mapNotNull { - val favoritePharmacy = it.find { it.telematikId == pharmacy.telematikId } - favoritePharmacy != null - }.flowOn(dispatcher) - - override suspend fun markAsRedeemed(taskId: String) { - withContext(dispatcher) { - dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { - val scannedTasks = it.toMutableList() - val index = scannedTasks.indexOfFirst { item -> item.taskId == taskId } - scannedTasks[index] = scannedTasks[index].copy(redeemedOn = Clock.System.now()) - scannedTasks - } - } - } -} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoRedeemLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoRedeemLocalDataSource.kt new file mode 100644 index 00000000..ce1f9bcb --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoRedeemLocalDataSource.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.repository.pharmacy + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock + +class DemoRedeemLocalDataSource( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : RedeemLocalDataSource { + + override suspend fun markAsRedeemed(taskId: String) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedTasks = it.toMutableList() + val index = scannedTasks.indexOfFirst { item -> item.taskId == taskId } + scannedTasks[index] = scannedTasks[index].copy(redeemedOn = Clock.System.now()) + scannedTasks + } + } + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoShippingContactRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoShippingContactRepository.kt new file mode 100644 index 00000000..ad6a1067 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoShippingContactRepository.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.demomode.repository.pharmacy + +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DemoShippingContactRepository : ShippingContactRepository { + override fun shippingContact(): Flow { + return flowOf( + PharmacyData.ShippingContact( + name = "Helga Schmetterling", + line1 = "Schmetterlingweg 1", + line2 = "2 Stockwerk rechts", + postalCode = "12345", + city = "Berlin", + telephoneNumber = "123456789", + mail = "schmetterling@butterfly.com", + deliveryInformation = "Bitte klingeln" + ) + ) + } + + override suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) { + // do nothing + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt index eda63295..000d2b18 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.repository.prescriptions @@ -23,6 +23,7 @@ import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS import de.gematik.ti.erp.app.demomode.model.DemoModeSentCommunicationJson import de.gematik.ti.erp.app.demomode.model.toDemoModeProfileLinkedCommunication import de.gematik.ti.erp.app.prescription.model.ScannedTaskData.ScannedTask +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.SyncedTask import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -35,23 +36,31 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.decodeFromJsonElement +private const val NOT_FOUND = -1 + class DemoPrescriptionsRepository( private val dataSource: DemoModeDataSource, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : PrescriptionRepository { override suspend fun saveScannedTasks( profileId: ProfileIdentifier, - tasks: List + tasks: List, + medicationString: String ) { + val updatedTasksWithNames = tasks.mapIndexed { index, scannedTask -> + scannedTask.copy(name = "$medicationString ${index + 1}") + } withContext(dispatcher) { dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { val scannedList = it.toMutableList() - scannedList.addAll(tasks) + scannedList.addAll(updatedTasksWithNames) scannedList } } @@ -68,8 +77,8 @@ class DemoPrescriptionsRepository( override suspend fun redeemPrescription( profileId: ProfileIdentifier, communication: JsonElement, - accessCode: String? - ): Result = + accessCode: String + ): Result = withContext(dispatcher) { val decodedCommunication = Json .decodeFromJsonElement(communication) @@ -78,10 +87,35 @@ class DemoPrescriptionsRepository( communications.add(decodedCommunication) communications } - Result.success(Unit) + // change the status of the prescription to in progress + dataSource.syncedTasks.value = dataSource.syncedTasks.updateAndGet { syncedList -> + val index = syncedList.indexOfFirst { profileId == it.profileId && it.taskId == decodedCommunication.taskId } + if (index != NOT_FOUND) { + val updatedItem = syncedList[index].copy( + status = SyncedTaskData.TaskStatus.InProgress, + lastModified = Clock.System.now() + ) + syncedList[index] = updatedItem + } + syncedList + } + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { scannedList -> + val index = scannedList.indexOfFirst { profileId == it.profileId && it.taskId == decodedCommunication.taskId } + if (index != NOT_FOUND) { + val updatedItem = scannedList[index].copy( + redeemedOn = Clock.System.now() + ) + scannedList[index] = updatedItem + } + scannedList + } + Result.success(JsonPrimitive(true)) // sending some random json response } - override suspend fun deleteTaskByTaskId(profileId: ProfileIdentifier, taskId: String): Result = + override suspend fun deleteRemoteTaskById( + profileId: ProfileIdentifier, + taskId: String + ): Result = withContext(dispatcher) { dataSource.syncedTasks.value = dataSource.syncedTasks.updateAndGet { syncedList -> syncedList.removeIf { it.taskId == taskId && it.profileId == profileId } @@ -93,7 +127,7 @@ class DemoPrescriptionsRepository( .removeIf { scannedItem -> scannedItem.taskId == taskId && scannedItem.profileId == profileId } scannedList } - Result.success(Unit) + Result.success(null) } // used only for scanned @@ -142,4 +176,22 @@ class DemoPrescriptionsRepository( syncedTasks.mapNotNull { it.taskId } ) }.flowOn(dispatcher) + + override suspend fun deleteLocalTaskById(taskId: String) { + // do nothing + } + + override suspend fun wasProfileEverAuthenticated(profileId: ProfileIdentifier): Boolean { + return true + } + + override suspend fun redeemScannedTasks(taskIds: List) { + // do nothing + } + + override fun loadAllTaskIds(profileId: ProfileIdentifier): Flow> = loadTaskIds() + + override suspend fun deleteLocalInvoicesById(taskId: String) { + // do nothing + } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt index 1188026d..cc821da9 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MagicNumber") @@ -34,7 +34,7 @@ class DemoTaskRepository( ) : TaskRepository { override suspend fun downloadTasks(profileId: ProfileIdentifier): Result = withContext(dispatcher) { - delay(500) + delay(250) Result.success(0) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt index 4f267f03..7c2c1fa7 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt @@ -1,25 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.demomode.repository.profiles import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.create +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoEmptyProfile import de.gematik.ti.erp.app.demomode.model.DemoModeProfile import de.gematik.ti.erp.app.demomode.model.toProfile import de.gematik.ti.erp.app.demomode.model.toProfiles @@ -33,6 +35,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.withContext @@ -63,6 +66,17 @@ class DemoProfilesRepository( } } + override suspend fun createNewProfile(profileName: String) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + profiles.add(profileName.create()) + profiles + } + } + } + override suspend fun activateProfile(profileId: ProfileIdentifier) { withContext(dispatcher) { dataSource.profiles.value = dataSource.profiles @@ -74,10 +88,13 @@ class DemoProfilesRepository( } } - override suspend fun removeProfile(profileId: ProfileIdentifier) { + override suspend fun removeProfile(profileId: ProfileIdentifier, profileName: String) { withContext(dispatcher) { dataSource.profiles.value = dataSource.profiles .updateAndGet { profiles -> + if (profiles.size == 1) { + profiles.add(demoEmptyProfile(profileName)) + } profiles.removeIf { profile -> profile.id == profileId } profiles } @@ -90,7 +107,18 @@ class DemoProfilesRepository( insuranceIdentifier: String, insuranceName: String ) { - // Not used in demo mode + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { + it.replace( + profileId = profileId, + insurantName = insurantName, + insuranceIdentifier = insuranceIdentifier, + insuranceName = insuranceName + ) + } + .updateUUIDForChangeVisibility() + } } override suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) { @@ -146,12 +174,29 @@ class DemoProfilesRepository( } } - override suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { - // Not for demo mode, will come later + override suspend fun switchProfileToPKV(profileId: ProfileIdentifier): Boolean { + // cannot switch to PKV in demo mode + return false + } + + override suspend fun switchProfileToGKV(profileId: ProfileIdentifier): Boolean { + return true } override suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean = false + override fun getProfileById(profileId: ProfileIdentifier): Flow = + demoModeProfiles().mapNotNull { + it.find { + profile -> + profile.id == profileId + }?.toProfile() + } + + override suspend fun isSsoTokenValid(profileId: ProfileIdentifier): Flow { + return flowOf(true) + } + private fun MutableList.index(profileId: ProfileIdentifier) = indexOfFirst { profile -> profile.id == profileId } .takeIf { it != INDEX_OUT_OF_BOUNDS } @@ -164,7 +209,10 @@ class DemoProfilesRepository( lastAuthenticated: Instant? = null, avatar: ProfilesData.Avatar? = null, profileImage: ByteArray? = null, - imageAction: ImageActions = NoAction + imageAction: ImageActions = NoAction, + insurantName: String? = null, + insuranceIdentifier: String? = null, + insuranceName: String? = null ): MutableList = index(profileId)?.let { index -> val existingProfile = this[index] @@ -174,6 +222,9 @@ class DemoProfilesRepository( name = name ?: existingProfile.name, color = color ?: existingProfile.color, lastAuthenticated = lastAuthenticated, + insurantName = insurantName ?: existingProfile.insurantName, + insuranceIdentifier = insuranceIdentifier ?: existingProfile.insuranceIdentifier, + insuranceName = insuranceName ?: existingProfile.insuranceName, avatar = avatar ?: existingProfile.avatar, personalizedImage = when (imageAction) { Add -> profileImage diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt index 4c1767a0..94d3fc37 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.repository.protocol diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt index ec5dbf85..3a63bb86 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.ui diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt index f455ab86..02c4eaaa 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.demomode.usecase.idp diff --git a/app/features/build.gradle.kts b/app/features/build.gradle.kts index 5287d387..c0def240 100644 --- a/app/features/build.gradle.kts +++ b/app/features/build.gradle.kts @@ -1,217 +1,28 @@ -@file:Suppress("UnstableApiUsage") - -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import org.owasp.dependencycheck.reporting.ReportGenerator.Format +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin plugins { - id("com.android.library") - kotlin("android") - kotlin("plugin.serialization") - id("org.jetbrains.compose") - id("io.realm.kotlin") - id("kotlin-parcelize") - id("org.owasp.dependencycheck") - id("com.jaredsburrows.license") - id("de.gematik.ti.erp.dependencies") - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("de.gematik.ti.erp.technical-requirements") + id("base-android-library") + id("de.gematik.ti.erp.names") + id("jacoco") + alias(libs.plugins.paparazzi) } -licenseReport { - generateCsvReport = false - generateHtmlReport = false - generateJsonReport = true - copyJsonReportToAssets = true -} +val namesPlugin = AppDependencyNamesPlugin() android { - namespace = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.features" + namespace = namesPlugin.moduleName("features") defaultConfig { - testApplicationId = "de.gematik.ti.erp.app.test" - } - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - dependencyCheck { - analyzers.assemblyEnabled = false - suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" - formats = listOf(Format.HTML, Format.XML) - scanConfigurations = configurations.filter { - it.name.startsWith("api") || - it.name.startsWith("implementation") || - it.name.startsWith("kapt") - }.map { it.name } - } - buildTypes { - val debug by getting { - isJniDebuggable = true - } - create("minifiedDebug") { - initWith(debug) - } - } - buildFeatures { - compose = true + testApplicationId = namesPlugin.moduleName("test") } } dependencies { - implementation(project(":common")) - implementation(project(":app:demo-mode")) - implementation("com.google.android.material:material:1.10.0") - testImplementation(project(":common")) - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) - testImplementation(kotlin("test")) - implementation("com.tom-roush:pdfbox-android:2.0.27.0") { - exclude(group = "org.bouncycastle") - } - // TODO: Make a common inject for all libs that we don't need to copy again and again - inject { - dataMatrix { - implementation(mlkitBarcodeScanner) - implementation(zxing) - } - coroutines { - implementation(coroutinesCore) - implementation(coroutinesAndroid) - implementation(coroutinesPlayServices) - } - dateTime { - implementation(datetime) - testCompileOnly(datetime) - } - accompanist { - implementation(navigationMaterial) - implementation(swipeRefresh) - implementation(flowLayout) - implementation(pager) - implementation(pageIndicator) - implementation(systemUiController) - } - android { - implementation(imageCropper) - debugImplementation(processPhoenix) - } - androidX { - implementation(legacySupport) - implementation(appcompat) - implementation(coreKtx) - implementation(datastorePreferences) - implementation(security) - implementation(biometric) - implementation(webkit) - - implementation(lifecycleViewmodel) - implementation(lifecycleComposeRuntime) - implementation(lifecycleProcess) - implementation(composeNavigation) - implementation(composeActivity) - implementation(composePaging) - implementation(camerax2) - implementation(cameraxLifecycle) - implementation(cameraxView) - } - dependencyInjection { - compileOnly(kodeinCompose) - implementation(kodeinCompose) - implementation(kodeinViewModel) - implementation(kodeinAndroid) - implementation(kodein) - androidTestImplementation(kodeinCompose) - } - imageLoad { - implementation(coil) - } - logging { - implementation(napier) - } - lottie { - implementation(lottie) - } - serialization { - implementation(kotlinXJson) - } - crypto { - implementation(jose4j) - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - testImplementation(bouncycastleBcprov) - testImplementation(bouncycastleBcpkix) - } - network { - implementation(retrofit) - implementation(retrofit2KotlinXSerialization) - implementation(okhttp3) - implementation(okhttpLogging) - // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). - // Can be removed as soon as Retrofit releases a new version >2.9.0. - implementation(okio) - androidTestImplementation(okhttp3) - } - database { - compileOnly(realm) - testCompileOnly(realm) - } - compose { - implementation(runtime) - implementation(foundation) - implementation(animation) - implementation(uiTooling) - implementation(preview) - } - material { - implementation(material) - implementation(material3) - implementation(materialIcons) - implementation(materialIconsExtended) - } - passwordStrength { - implementation(zxcvbn) - } - stateManagement { - implementation(reactiveState) - } - tracking { - implementation(contentSquare) - implementation(contentSquareCompose) - implementation(contentSquareErrorAnalysis) - } - maps { - implementation(location) - implementation(maps) - implementation(mapsAndroidUtils) - implementation(mapsKtx) - implementation(mapsCompose) - } - playServices { - implementation(integrity) - implementation(appReview) - implementation(appUpdate) - } - shimmer { - implementation(shimmer) - } - networkTest { - testImplementation(mockWebServer) - } - test { - testImplementation(junit4) - testImplementation(snakeyaml) - testImplementation(json) - testImplementation(mockk) - androidTestImplementation(mockkAndroid) - } - } -} - -secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) { - "ci-overrides.properties" - } else { - "gradle.properties" - } + implementation(project(namesPlugin.demoMode)) + implementation(project(namesPlugin.testTags)) + implementation(project(namesPlugin.multiplatform)) + implementation(project(namesPlugin.uiComponents)) + implementation(libs.androidx.work) + implementation(libs.certificatetransparency.android) + testImplementation(libs.test.turbine) // to test flows + testImplementation(project(namesPlugin.multiplatform)) } diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt index ed05ed2f..e50ab6d8 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di @@ -23,6 +23,9 @@ import androidx.core.content.edit import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.debugsettings.data.Environment +/** + * Documentation: documentation-internal/variants/build_variants.adoc + */ class EndpointHelper( private val networkPrefs: SharedPreferences ) { @@ -57,12 +60,17 @@ class EndpointHelper( url = networkPrefs.getString( uri.preferenceKey, uri.original - )!! + ) ?: "" } - if (url.last() != '/') { - url += '/' + return when { + url.isEmpty() -> "https://github.com/gematik/E-Rezept-App-Android/" + url.last() != '/' -> { + url += '/' + url + } + + else -> return url } - return url } private fun overrideSwitchKey(uri: EndpointUri): String { @@ -80,6 +88,7 @@ class EndpointHelper( } } + @Suppress("CyclomaticComplexMethod") fun getCurrentEnvironment(): Environment { return when { eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_PU && @@ -87,32 +96,46 @@ class EndpointHelper( pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_PU -> { Environment.PU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.RU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU_DEV && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU_DEV && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.RUDEV } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TU && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TU && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.TU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TR && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TR && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.TR } + else -> { return Environment.PU } } } + fun getOrganDonationRegisterIntentHost() = + if (getCurrentEnvironment() == Environment.PU) { + BuildKonfig.ORGAN_DONATION_REGISTER_PU + } else { + BuildKonfig.ORGAN_DONATION_REGISTER_RU + } + + fun getOrganDonationRegisterInfoHost() = BuildKonfig.ORGAN_DONATION_INFO + fun getErpApiKey(): String { return if (BuildKonfig.INTERNAL) { when (getCurrentEnvironment()) { diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt index e2ee35eb..07a1f755 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt @@ -1,25 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file: Suppress("UnusedPrivateMember", "MagicNumber") + package de.gematik.ti.erp.app.ui import android.content.Intent import android.net.Uri +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -45,13 +48,17 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Switch import androidx.compose.material.Text -import androidx.compose.material.TextField import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -74,23 +81,30 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.debugsettings.logger.ui.screens.LoggerScreen import de.gematik.ti.erp.app.debugsettings.navigation.DebugScreenNavigation +import de.gematik.ti.erp.app.debugsettings.pkv.ui.DebugScreenPKV +import de.gematik.ti.erp.app.debugsettings.qrcode.QrCodeScannerScreen import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen -import de.gematik.ti.erp.app.debugsettings.ui.DebugScreenPKV -import de.gematik.ti.erp.app.debugsettings.ui.EnvironmentSelector -import de.gematik.ti.erp.app.debugsettings.ui.LoadingButton +import de.gematik.ti.erp.app.debugsettings.ui.components.ClearTextTrafficSection +import de.gematik.ti.erp.app.debugsettings.ui.components.ClientIdsSection +import de.gematik.ti.erp.app.debugsettings.ui.components.EnvironmentSelector +import de.gematik.ti.erp.app.debugsettings.ui.components.LoadingButton import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.ui.LabelButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.AlertDialog import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.LabelButton import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.erezeptTextFieldColors import de.gematik.ti.erp.app.utils.compose.navigationModeState +import de.gematik.ti.erp.app.utils.extensions.erezeptColors import kotlinx.coroutines.launch import org.bouncycastle.util.encoders.Base64 import org.kodein.di.bindProvider @@ -187,6 +201,7 @@ fun EditablePathComponentWithControl( onValueChange = { onValueChange(it, false) }, label = { Text(label) }, maxLines = 3, + colors = erezeptTextFieldColors(), modifier = Modifier .weight(1f) .padding(end = PaddingDefaults.Medium) @@ -210,7 +225,7 @@ fun DebugScreen( endpointHelper = instance(), cardWallUseCase = instance(), prescriptionUseCase = instance(), - invoiceRepository = instance(), + saveInvoiceUseCase = instance(), vauRepository = instance(), idpRepository = instance(), idpUseCase = instance(), @@ -223,6 +238,7 @@ fun DebugScreen( ) } }) { + val viewModel by rememberViewModel() NavHost( navController, startDestination = DebugScreenNavigation.DebugMain.path() @@ -230,6 +246,7 @@ fun DebugScreen( composable(DebugScreenNavigation.DebugMain.route) { NavigationAnimation(mode = navMode) { DebugScreenMain( + viewModel = viewModel, onBack = { settingsNavController.popBackStack() }, @@ -241,6 +258,12 @@ fun DebugScreen( }, onClickBioMetricSettings = { navController.navigate(DebugScreenNavigation.DebugTimeout.path()) + }, + onScanQrCode = { + navController.navigate(DebugScreenNavigation.QrCodeScannerScreen.path()) + }, + onClickLogger = { + navController.navigate(DebugScreenNavigation.LoggerScreen.path()) } ) } @@ -248,6 +271,7 @@ fun DebugScreen( composable(DebugScreenNavigation.DebugRedeemWithoutFD.route) { NavigationAnimation(mode = navMode) { DebugScreenDirectRedeem( + viewModel = viewModel, onBack = { navController.popBackStack() } @@ -255,7 +279,6 @@ fun DebugScreen( } } composable(DebugScreenNavigation.DebugPKV.route) { - val viewModel by rememberViewModel() NavigationAnimation(mode = navMode) { DebugScreenPKV( onSaveInvoiceBundle = { @@ -272,13 +295,32 @@ fun DebugScreen( navController.popBackStack() } } + composable(DebugScreenNavigation.QrCodeScannerScreen.route) { + QrCodeScannerScreen.Content( + onSaveCertificate = { viewModel.onSetVirtualHealthCardCertificate(it) }, + onSavePrivateKey = { viewModel.onSetVirtualHealthCardPrivateKey(it) }, + onBack = { + navController.popBackStack() + } + ) + } + composable(DebugScreenNavigation.LoggerScreen.route) { + LoggerScreen.Content( + onBack = { + navController.popBackStack() + } + ) + } } } } +// TODO: Change to use the correct use-cases @Composable -fun DebugScreenDirectRedeem(onBack: () -> Unit) { - val viewModel by rememberViewModel() +fun DebugScreenDirectRedeem( + viewModel: DebugSettingsViewModel, + onBack: () -> Unit +) { val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -307,26 +349,29 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Endpoints" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = shipmentUrl, - label = { Text("Shipment URL") }, + label = "Shipment URL", + placeholder = "Shipment URL", onValueChange = { shipmentUrl = it } ) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = deliveryUrl, - label = { Text("Delivery URL") }, + label = "Delivery URL", + placeholder = "Delivery URL", onValueChange = { deliveryUrl = it } ) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = onPremiseUrl, - label = { Text("OnPremise URL") }, + label = "OnPremise URL", + placeholder = "OnPremise URL", onValueChange = { onPremiseUrl = it } @@ -360,12 +405,13 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Message" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .heightIn(max = 400.dp) .fillMaxWidth(), value = message, - label = { Text("Any Message") }, + label = "Any Message", + placeholder = "Any Message", onValueChange = { message = it } @@ -376,12 +422,13 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Certificates" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .heightIn(max = 400.dp) .fillMaxWidth(), value = certificates, - label = { Text("Certificate as PEM") }, + label = "Certificate as PEM", + placeholder = "Certificate as PEM", onValueChange = { certificates = it } @@ -410,12 +457,14 @@ private fun RedeemButton( @OptIn(ExperimentalMaterialApi::class) @Composable fun DebugScreenMain( + viewModel: DebugSettingsViewModel, onBack: () -> Unit, onClickDirectRedemption: () -> Unit, onClickPKV: () -> Unit, - onClickBioMetricSettings: () -> Unit + onClickBioMetricSettings: () -> Unit, + onScanQrCode: () -> Unit, + onClickLogger: () -> Unit ) { - val viewModel by rememberViewModel() val listState = rememberLazyListState() val modal = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() @@ -473,7 +522,7 @@ fun DebugScreenMain( } LabelButton( icon = painterResource(R.drawable.ic_pkv), - text = "PKV" + text = "PKV / GKV Switch" ) { onClickPKV() } @@ -483,6 +532,12 @@ fun DebugScreenMain( ) { viewModel.refreshPrescriptions() } + LabelButton( + icon = Icons.Rounded.Adb, + text = "Logger" + ) { + onClickLogger() + } } } item { @@ -495,11 +550,11 @@ fun DebugScreenMain( ) { Text( text = "Original app update", - modifier = Modifier - .weight(1f) + modifier = Modifier.weight(1f) ) Switch( modifier = Modifier.testTag(TestTag.DebugMenu.FakeAppUpdate), + colors = SwitchDefaults.erezeptColors(), checked = appUpdateManager, onCheckedChange = { viewModel.changeAppUpdateManager(it) } ) @@ -521,6 +576,7 @@ fun DebugScreenMain( ) Switch( modifier = Modifier.testTag(TestTag.DebugMenu.FakeNFCCapabilities), + colors = SwitchDefaults.erezeptColors(), checked = viewModel.debugSettingsData.fakeNFCCapabilities, onCheckedChange = { viewModel.allowNfc(it) } ) @@ -565,7 +621,11 @@ fun DebugScreenMain( } } item { - VirtualHealthCard(viewModel = viewModel) + VirtualHealthCard( + viewModel = viewModel + ) { + onScanQrCode() + } } item { FeatureToggles(viewModel = viewModel) @@ -573,6 +633,18 @@ fun DebugScreenMain( item { RotatingLog(viewModel = viewModel) } + item { + HorizontalDivider() + } + item { + ClearTextTrafficSection() + } + item { + HorizontalDivider() + } + item { + ClientIdsSection() + } } } } @@ -614,7 +686,11 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV } @Composable -private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { +private fun VirtualHealthCard( + modifier: Modifier = Modifier, + viewModel: DebugSettingsViewModel, + onScanQrCode: () -> Unit +) { var virtualHealthCardLoading by remember { mutableStateOf(false) } var virtualHealthCardError by remember { mutableStateOf(null) } virtualHealthCardError?.let { error -> @@ -635,8 +711,19 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet DebugCard(modifier, title = "Virtual Health Card", onReset = viewModel::onResetVirtualHealthCard) { val scope = rememberCoroutineScope() + TextButton( + modifier = Modifier.fillMaxWidth(), + border = BorderStroke(1.dp, AppTheme.colors.primary600), + shape = RoundedCornerShape(8.dp), + onClick = onScanQrCode + ) { + Text( + text = "Scan Virtual Health Card", + color = AppTheme.colors.primary600 + ) + } - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .testTag(TestTag.DebugMenu.CertificateField) .heightIn(max = 144.dp) @@ -645,7 +732,8 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet onValueChange = { viewModel.onSetVirtualHealthCardCertificate(it) }, - label = { Text("Certificate in Base64") } + label = "Certificate in Base64", + placeholder = "Certificate in Base64" ) val subjectInfo = @@ -654,7 +742,7 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet } Text(subjectInfo, style = AppTheme.typography.caption1l) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .testTag(TestTag.DebugMenu.PrivateKeyField) .heightIn(max = 144.dp) @@ -663,7 +751,8 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet onValueChange = { viewModel.onSetVirtualHealthCardPrivateKey(it) }, - label = { Text("Private Key in Base64") } + label = "Private Key in Base64", + placeholder = "Private Key in Base64" ) Button( diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt index 76c0e894..f29baca7 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.ui diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt index 7589fbc8..9405737e 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.ui import android.content.Intent @@ -36,7 +38,7 @@ import de.gematik.ti.erp.app.VisibleDebugTree import de.gematik.ti.erp.app.appupdate.usecase.ChangeAppUpdateManagerFlagUseCase import de.gematik.ti.erp.app.appupdate.usecase.GetAppUpdateManagerFlagUseCase import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase -import de.gematik.ti.erp.app.data.DebugSettingsData +import de.gematik.ti.erp.app.debugsettings.data.DebugSettingsData import de.gematik.ti.erp.app.debugsettings.data.Environment import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager @@ -45,7 +47,7 @@ import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.AccessToken import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.invoice.usecase.SaveInvoiceUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -85,7 +87,7 @@ class DebugSettingsViewModel( private val prescriptionUseCase: PrescriptionUseCase, private val vauRepository: VauRepository, private val idpRepository: IdpRepository, - private val invoiceRepository: InvoiceRepository, + private val saveInvoiceUseCase: SaveInvoiceUseCase, private val idpUseCase: IdpUseCase, private val profilesUseCase: ProfilesUseCase, private val featureToggleManager: FeatureToggleManager, @@ -146,10 +148,10 @@ class DebugSettingsViewModel( } fun selectEnvironment(environment: Environment) { - updateState(getDebugSettingsdataForEnvironment(environment)) + updateState(getDebugSettingsDataForEnvironment(environment)) } - private fun getDebugSettingsdataForEnvironment(environment: Environment): DebugSettingsData { + private fun getDebugSettingsDataForEnvironment(environment: Environment): DebugSettingsData { return when (environment) { Environment.PU -> debugSettingsData.copy( eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_PU, @@ -241,9 +243,10 @@ class DebugSettingsViewModel( else -> it } idpRepository.saveSingleSignOnToken( - activeProfileId, - newToken + profileId = activeProfileId, + token = newToken ) + idpRepository.invalidateDecryptedAccessToken(activeProfileId) Napier.d("SSO token is now: $newToken", tag = "Debug Settings") } } @@ -338,6 +341,7 @@ class DebugSettingsViewModel( e.message ?: "Error" } + @OptIn(ExperimentalStdlibApi::class) suspend fun onTriggerVirtualHealthCard( certificateBase64: String, privateKeyBase64: String @@ -345,11 +349,17 @@ class DebugSettingsViewModel( idpUseCase.authenticationFlowWithHealthCard( profileId = profilesUseCase.activeProfileId().first(), cardAccessNumber = "123123", - healthCardCertificate = { Base64.decode(certificateBase64) }, + healthCardCertificate = { java.util.Base64.getDecoder().decode(certificateBase64) }, sign = { val curveSpec = ECNamedCurveTable.getParameterSpec("brainpoolP256r1") val keySpec = - org.bouncycastle.jce.spec.ECPrivateKeySpec(BigInteger(Base64.decode(privateKeyBase64)), curveSpec) + org.bouncycastle.jce.spec.ECPrivateKeySpec( + BigInteger( + 1, + java.util.Base64.getDecoder().decode(privateKeyBase64) + ), + curveSpec + ) val privateKey = KeyFactory.getInstance("EC", BCProvider).generatePrivate(keySpec) val signed = Signature.getInstance("NoneWithECDSA").apply { initSign(privateKey) @@ -388,7 +398,7 @@ class DebugSettingsViewModel( viewModelScope.launch { val profileId = profilesUseCase.activeProfileId().first() val bundle = Json.parseToJsonElement(invoiceBundle) - invoiceRepository.saveInvoice(profileId, bundle) + saveInvoiceUseCase.invoke(profileId, bundle) } } diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt index f358586a..3f0074e5 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -32,19 +32,12 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.BugReport import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.key -import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints @@ -54,7 +47,8 @@ import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import java.util.UUID +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny @Composable fun OutlinedDebugButton( @@ -76,25 +70,6 @@ fun OutlinedDebugButton( } } -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.visualTestTag(tag: String) = - composed(fullyQualifiedName = "de.gematik.ti.erp.app.utils.compose.visualTestTag", key1 = tag) { - val activity = LocalContext.current as MainActivity - val uuid = remember { UUID.randomUUID().toString() } - - DisposableEffect(tag) { - onDispose { - activity.elementsUsedInTests -= uuid - } - } - - Modifier - .testTag(tag) - .onGloballyPositioned { - activity.elementsUsedInTests += uuid to MainActivity.ElementForTest(it.boundsInRoot(), tag) - } - } - @Composable fun DebugOverlay(elements: Map) { Box(Modifier.fillMaxSize()) { diff --git a/app/features/src/main/AndroidManifest.xml b/app/features/src/main/AndroidManifest.xml index d68ab4e4..8cea483d 100644 --- a/app/features/src/main/AndroidManifest.xml +++ b/app/features/src/main/AndroidManifest.xml @@ -1,17 +1,29 @@ - + + - + + + + + + + + + + + + android:value="false" /> + android:grantUriPermissions="true" + android:localeConfig="@xml/locale_config"> @@ -19,24 +31,26 @@ + android:screenOrientation="portrait" + android:usesCleartextTraffic="false" + android:windowSoftInputMode="adjustResize" + tools:ignore="DiscouragedApi"> - + - + - + - + - - + + + + + + + + + + - \ No newline at end of file + diff --git a/app/features/src/main/assets/ar.lproj/internal_messages.json b/app/features/src/main/assets/ar.lproj/internal_messages.json new file mode 100644 index 00000000..4eae05fb --- /dev/null +++ b/app/features/src/main/assets/ar.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "من الآن فصاعدًا، استخدم تطبيق الوصفات الطبية الإلكترونية لتسجيل قرارك بالتبرع بالأعضاء في **سجل التبرع بالأعضاء الرقمي** - بأمان وسهولة.\n\nيوجد حاليًا 9192 شخصًا في ألمانيا ينتظرون بشكل عاجل الحصول على عضو - كل قرار مهم ويمكن أن ينقذ الأرواح.\n\nاتخذ قرارك اليوم وقدم مساهمة مهمة لمستقبل العديد من الأشخاص! ❤️‍🩹\n\nيمكن العثور على سجل التبرع بالأعضاء في علامة التبويب \"الإعدادات\" في هذا التطبيق.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/bg.lproj/internal_messages.json b/app/features/src/main/assets/bg.lproj/internal_messages.json new file mode 100644 index 00000000..1df12068 --- /dev/null +++ b/app/features/src/main/assets/bg.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Отсега нататък използвайте приложението за електронна рецепта, за да запишете решението си да дарите органи в **цифровия регистър за даряване на органи** - безопасно и лесно.\n\nВ момента в Германия 9192 души чакат спешно за орган - всяко решение има значение и може да спаси животи.\n\nВземете своето решение днес и направете важен принос за бъдещето на много хора! ❤️‍🩹\n\nРегистърът за даряване на органи може да бъде намерен в раздела „Настройки“ в това приложение.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/cs.lproj/internal_messages.json b/app/features/src/main/assets/cs.lproj/internal_messages.json new file mode 100644 index 00000000..d640fd6c --- /dev/null +++ b/app/features/src/main/assets/cs.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Od nynějška používejte aplikaci e-receptu k zaznamenání svého rozhodnutí darovat orgány do **digitálního registru dárcovství orgánů** – bezpečně a snadno.\n\nV současné době v Německu naléhavě čeká 9 192 lidí na orgán – každé rozhodnutí se počítá a může zachránit životy.\n\nUdělejte své rozhodnutí ještě dnes a přispějte významným způsobem k budoucnosti mnoha lidí! ❤️‍🩹\n\nRegistr darování orgánů najdete v této aplikaci na záložce „Nastavení“.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/da.lproj/internal_messages.json b/app/features/src/main/assets/da.lproj/internal_messages.json new file mode 100644 index 00000000..76f74d16 --- /dev/null +++ b/app/features/src/main/assets/da.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Fra nu af kan du bruge e-recept-appen til at registrere din beslutning om at donere organer i det **digitale organdonationsregister** - sikkert og nemt.\n\nDer er i øjeblikket 9.192 mennesker i Tyskland, der akut venter på et organ - hver beslutning tæller og kan redde liv.\n\nTræf din beslutning i dag og giv et vigtigt bidrag til mange menneskers fremtid! ❤️‍🩹\n\nOrgandonationsregistret kan findes under fanen \"Indstillinger\" i denne app.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/data_terms_en.html b/app/features/src/main/assets/data_terms_en.html index cc3a760c..25a8a6a6 100644 --- a/app/features/src/main/assets/data_terms_en.html +++ b/app/features/src/main/assets/data_terms_en.html @@ -77,7 +77,7 @@

Data protection

Further information

Gematik's official information on e-prescriptions in Germany can be found at www.das-e-rezept-fuer-deutschland.de + href="https://www.das-e-rezept-fuer-deutschland.de">www.das-e-rezept-fuer-deutschland.de . There you will receive understandable information and answers to frequently asked questions about e-prescriptions.

About this privacy policy

diff --git a/app/features/src/main/assets/data_terms_nl.html b/app/features/src/main/assets/data_terms_nl.html index ba43e49f..952fc216 100644 --- a/app/features/src/main/assets/data_terms_nl.html +++ b/app/features/src/main/assets/data_terms_nl.html @@ -76,7 +76,7 @@

Gegevensbescherming

Verdere informatie

Officiële informatie van Gematik over e-recepten in Duitsland kunt u vinden op www.das-e-rezept-fuer-deutschland.de + href="https://www.das-e-rezept-fuer-deutschland.de">www.das-e-rezept-fuer-deutschland.de . Daar krijgt u begrijpelijke informatie en antwoorden op veelgestelde vragen over e-recepten.

Over dit privacybeleid

diff --git a/app/features/src/main/assets/data_terms_pl.html b/app/features/src/main/assets/data_terms_pl.html index 43864376..87235c3c 100644 --- a/app/features/src/main/assets/data_terms_pl.html +++ b/app/features/src/main/assets/data_terms_pl.html @@ -79,7 +79,7 @@

Ochrona danych

Dalsza informacja

Oficjalne informacje firmy Gematik na temat e-recept w Niemczech można znaleźć na stronie www.das-e-rezept-fuer-deutschland.de + href="https://www.das-e-rezept-fuer-deutschland.de">www.das-e-rezept-fuer-deutschland.de . Otrzymasz tam zrozumiałe informacje i odpowiedzi na najczęściej zadawane pytania dotyczące e-recept.

O tej polityce prywatności

diff --git a/app/features/src/main/assets/de.lproj/internal_messages.json b/app/features/src/main/assets/de.lproj/internal_messages.json new file mode 100644 index 00000000..ec3552a6 --- /dev/null +++ b/app/features/src/main/assets/de.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Nutzen Sie ab sofort die E-Rezept App, um Ihre Entscheidung zur Organspende im **digitalen Organspenderegister** festzuhalten – sicher und unkompliziert.\u2028\n\nAktuell warten 9.192 Menschen in Deutschland dringend auf ein Organ – jede Entscheidung zählt und kann Leben retten.\n\nTreffen Sie heute Ihre Entscheidung und leisten Sie einen wichtigen Beitrag zur Zukunft vieler Menschen! ❤️‍🩹\n\nDas Organspenderegister finden Sie im Tab „Einstellungen“ in dieser App.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/en.lproj/internal_messages.json b/app/features/src/main/assets/en.lproj/internal_messages.json new file mode 100644 index 00000000..5adae32e --- /dev/null +++ b/app/features/src/main/assets/en.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Use the e-prescription app now to record your decision to donate organs in the **digital organ donation register** – safely and easily.\n\nThere are currently 9,192 people in Germany waiting urgently for an organ – every decision counts and can save lives.\n\nMake your decision today and make an important contribution to the future of many people! ❤️‍🩹\n\nYou can find the organ donation register in the “Settings” tab in this app.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/es.lproj/internal_messages.json b/app/features/src/main/assets/es.lproj/internal_messages.json new file mode 100644 index 00000000..91b1a15b --- /dev/null +++ b/app/features/src/main/assets/es.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "A partir de ahora, utilice la aplicación de recetas electrónicas para registrar su decisión de donar órganos en el **registro digital de donación de órganos**, de forma segura y sencilla.\n\nActualmente hay 9.192 personas en Alemania esperando urgentemente un órgano: cada decisión cuenta y puede salvar vidas.\n\n¡Toma tu decisión hoy y haz una contribución importante al futuro de muchas personas! ❤️‍🩹\n\nEl registro de donación de órganos se puede encontrar en la pestaña \"Configuración\" de esta aplicación.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/fr.lproj/internal_messages.json b/app/features/src/main/assets/fr.lproj/internal_messages.json new file mode 100644 index 00000000..aeba9565 --- /dev/null +++ b/app/features/src/main/assets/fr.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Utilisez désormais l'application de prescription électronique pour enregistrer votre décision de donner vos organes dans le **registre numérique des dons d'organes** - en toute sécurité et facilement.\n\nEn Allemagne, 9 192 personnes attendent actuellement un organe de toute urgence : chaque décision compte et peut sauver des vies.\n\nPrenez votre décision aujourd'hui et apportez une contribution importante à l'avenir de nombreuses personnes ! ❤️‍🩹\n\nLe registre des dons d'organes se trouve dans l'onglet « Paramètres » de cette application.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/ga.lproj/internal_messages.json b/app/features/src/main/assets/ga.lproj/internal_messages.json new file mode 100644 index 00000000..be786d12 --- /dev/null +++ b/app/features/src/main/assets/ga.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "As seo amach, bain úsáid as an aip ríomh-oidis chun do chinneadh chun orgáin a dheonú a thaifeadadh sa **clár deonaithe orgán digiteach** - go sábháilte agus go héasca.\n\nFaoi láthair tá 9,192 duine sa Ghearmáin ag fanacht go práinneach le horgán - tá gach cinneadh san áireamh agus féadann sé daoine a shábháil.\n\nDéan do chinneadh inniu agus cuir go mór le todhchaí go leor daoine! ❤️‍🩹\n\nTá an clár deonaithe orgán le fáil sa chluaisín “Socruithe” san aip seo.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/he.lproj/internal_messages.json b/app/features/src/main/assets/he.lproj/internal_messages.json new file mode 100644 index 00000000..225c3114 --- /dev/null +++ b/app/features/src/main/assets/he.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "מעכשיו, השתמשו באפליקציית המרשם האלקטרוני כדי לרשום את החלטתכם לתרום איברים ב**מרשם תרומות איברים דיגיטלי** - בצורה בטוחה וקלה.\n\nיש כיום 9,192 אנשים בגרמניה שמחכים בדחיפות לאיבר - כל החלטה חשובה ויכולה להציל חיים.\n\nקבל את ההחלטה שלך היום ותרום תרומה חשובה לעתידם של אנשים רבים! ❤️‍🩹\n\nאת מרשם תרומת האיברים ניתן למצוא בלשונית \"הגדרות\" באפליקציה זו.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/it.lproj/internal_messages.json b/app/features/src/main/assets/it.lproj/internal_messages.json new file mode 100644 index 00000000..c75f909f --- /dev/null +++ b/app/features/src/main/assets/it.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "D'ora in poi registrate con l'app e-prescription la vostra decisione di donare gli organi nel **registro digitale delle donazioni di organi** – in modo semplice e sicuro.\n\nAttualmente in Germania ci sono 9.192 persone in attesa urgente di un organo: ogni decisione conta e può salvare vite umane.\n\nPrendi la tua decisione oggi e dai un contributo importante al futuro di tante persone! ❤️‍🩹\n\nIl registro delle donazioni di organi si trova nella scheda \"Impostazioni\" di questa app.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/nl.lproj/internal_messages.json b/app/features/src/main/assets/nl.lproj/internal_messages.json new file mode 100644 index 00000000..3eb80dfa --- /dev/null +++ b/app/features/src/main/assets/nl.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Met de e-receptenapp legt u voortaan veilig en eenvoudig uw besluit om organen te doneren vast in het **digitale orgaandonatieregister**.\n\nMomenteel wachten 9.192 mensen in Duitsland met spoed op een orgaan. Elke beslissing telt en kan levens redden.\n\nNeem vandaag nog uw beslissing en lever een belangrijke bijdrage aan de toekomst van veel mensen! ❤️‍🩹\n\nHet orgaandonatieregister is te vinden op het tabblad 'Instellingen' in deze app.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/pl.lproj/internal_messages.json b/app/features/src/main/assets/pl.lproj/internal_messages.json new file mode 100644 index 00000000..26dd75f5 --- /dev/null +++ b/app/features/src/main/assets/pl.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Od teraz możesz korzystać z aplikacji e-recepty i rejestrować swoją decyzję o oddaniu narządów w **cyfrowym rejestrze dawstwa narządów** – bezpiecznie i łatwo.\n\nObecnie w Niemczech 9 192 osób pilnie oczekuje na narząd – każda decyzja ma znaczenie i może uratować życie.\n\nPodejmij decyzję już dziś i wnieś istotny wkład w przyszłość wielu ludzi! ❤️‍🩹\n\nRejestr dawstwa narządów można znaleźć w zakładce „Ustawienia” w tej aplikacji.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/ro.lproj/internal_messages.json b/app/features/src/main/assets/ro.lproj/internal_messages.json new file mode 100644 index 00000000..2807bd59 --- /dev/null +++ b/app/features/src/main/assets/ro.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "De acum înainte, utilizați aplicația de prescriere electronică pentru a vă înregistra decizia de a dona organe în **registrul digital al donării de organe** - în siguranță și ușor.\n\nÎn prezent, în Germania sunt 9.192 de persoane care așteaptă urgent un organ - fiecare decizie contează și poate salva vieți.\n\nLuați-vă decizia astăzi și aduceți o contribuție importantă la viitorul multor oameni! ❤️‍🩹\n\nRegistrul donării de organe poate fi găsit în fila „Setări” din această aplicație.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/ru.lproj/internal_messages.json b/app/features/src/main/assets/ru.lproj/internal_messages.json new file mode 100644 index 00000000..24e502e2 --- /dev/null +++ b/app/features/src/main/assets/ru.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Отныне используйте приложение электронного рецепта, чтобы записать свое решение пожертвовать органы в **цифровой реестр донорства органов** - безопасно и легко.\n\nВ настоящее время в Германии 9 192 человека срочно ждут органа. Каждое решение имеет значение и может спасти жизни.\n\nПримите решение сегодня и внесите важный вклад в будущее многих людей! ❤️‍🩹\n\nРеестр донорства органов можно найти на вкладке «Настройки» в этом приложении.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/terms_of_use.html b/app/features/src/main/assets/terms_of_use.html index 82b7e0be..30324f68 100644 --- a/app/features/src/main/assets/terms_of_use.html +++ b/app/features/src/main/assets/terms_of_use.html @@ -3,7 +3,7 @@ 2021-07-02_Nutzungsbedingungen E-Rezept-App - +

Nutzungsbedingungen E-Rezept-App

diff --git a/app/features/src/main/assets/tr.lproj/internal_messages.json b/app/features/src/main/assets/tr.lproj/internal_messages.json new file mode 100644 index 00000000..fd35bdf5 --- /dev/null +++ b/app/features/src/main/assets/tr.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Artık organ bağışlama kararınızı **dijital organ bağışı kaydına** güvenli ve kolay bir şekilde kaydetmek için e-reçete uygulamasını kullanın.\n\nŞu anda Almanya'da acilen organ bekleyen 9.192 kişi var; her karar önemlidir ve hayat kurtarabilir.\n\nKararınızı bugün verin ve birçok insanın geleceğine önemli bir katkıda bulunun! ❤️‍🩹\n\nOrgan bağışı kaydına bu uygulamadaki “Ayarlar” sekmesinden ulaşılabilir.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/uk.lproj/internal_messages.json b/app/features/src/main/assets/uk.lproj/internal_messages.json new file mode 100644 index 00000000..0c0fabf1 --- /dev/null +++ b/app/features/src/main/assets/uk.lproj/internal_messages.json @@ -0,0 +1,8 @@ +[ + { + "id": "2", + "text": "Відтепер використовуйте додаток для електронних рецептів, щоб записати своє рішення про донорство органів у **цифровому реєстрі донорства** - безпечно та легко.\n\nЗараз у Німеччині 9192 людини терміново чекають на орган - кожне рішення має значення та може врятувати життя.\n\nПрийміть рішення сьогодні та зробіть важливий внесок у майбутнє багатьох людей! ❤️‍🩹\n\nРеєстр донорства органів можна знайти у вкладці «Налаштування» в цьому додатку.", + "timestamp": "2024-11-29T08:00:00.000Z", + "version": "1.26.0" + } +] \ No newline at end of file diff --git a/app/features/src/main/kotlin/android/print/PdfPrinter.kt b/app/features/src/main/kotlin/android/print/PdfPrinter.kt index f96c1d00..59075cb0 100644 --- a/app/features/src/main/kotlin/android/print/PdfPrinter.kt +++ b/app/features/src/main/kotlin/android/print/PdfPrinter.kt @@ -1,3 +1,21 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + package android.print import android.os.CancellationSignal @@ -12,20 +30,26 @@ import kotlin.coroutines.suspendCoroutine */ class PdfPrint(private val printAttributes: PrintAttributes) { suspend fun print(printAdapter: PrintDocumentAdapter, outputFd: File) = suspendCoroutine { continuation -> - printAdapter.onLayout(null, printAttributes, null, object : PrintDocumentAdapter.LayoutResultCallback() { - override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) { - printAdapter.onWrite( - arrayOf(PageRange.ALL_PAGES), - ParcelFileDescriptor.open(outputFd, ParcelFileDescriptor.MODE_READ_WRITE), - CancellationSignal(), - object : PrintDocumentAdapter.WriteResultCallback() { - override fun onWriteFinished(pages: Array) { - super.onWriteFinished(pages) - continuation.resumeWith(Result.success(Unit)) + printAdapter.onLayout( + null, + printAttributes, + null, + object : PrintDocumentAdapter.LayoutResultCallback() { + override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) { + printAdapter.onWrite( + arrayOf(PageRange.ALL_PAGES), + ParcelFileDescriptor.open(outputFd, ParcelFileDescriptor.MODE_READ_WRITE), + CancellationSignal(), + object : PrintDocumentAdapter.WriteResultCallback() { + override fun onWriteFinished(pages: Array) { + super.onWriteFinished(pages) + continuation.resumeWith(Result.success(Unit)) + } } - } - ) - } - }, null) + ) + } + }, + null + ) } -} \ No newline at end of file +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/CrashCollector.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/CrashCollector.kt index fa460fef..928cb02c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/CrashCollector.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/CrashCollector.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app @@ -23,11 +23,21 @@ import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension /** * Any exception that is not thrown correctly is caught here as a fallback mechanism */ -fun MainActivity.catchAllUnCaughtExceptions() { +fun MainActivity.catchAllUnCaughtExceptions(activity: MainActivity) { if (BuildConfigExtension.isReleaseMode) { + val isDemoMode = activity.isDemoMode() + val packageName = activity.packageName + activity.isDestroyed val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - defaultExceptionHandler!!.uncaughtException(thread, MessageConversionException(throwable)) + defaultExceptionHandler?.uncaughtException( + thread, + UncaughtException( + isDemoMode = isDemoMode, + packageName = packageName, + throwable = throwable + ) + ) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/DomainVerifier.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/DomainVerifier.kt index 591dbaec..0deaaf86 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/DomainVerifier.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/DomainVerifier.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app @@ -23,6 +23,7 @@ import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState import android.os.Build import androidx.annotation.RequiresApi +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension interface DomainVerifier { val areDomainsVerified: Boolean @@ -55,7 +56,13 @@ data class Sdk31DomainVerifier( private val requiredDomainsSize = userState?.hostToStateMap ?.filterValues { it == DomainVerificationUserState.DOMAIN_STATE_SELECTED }?.size ?: 0 - override val areDomainsVerified: Boolean = true + override val areDomainsVerified: Boolean + get() = + if (BuildConfigExtension.isInternalDebug) { + verifiedDomainsSize != 0 || requiredDomainsSize != 0 + } else { + true + } } /** diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt index d2d67d8d..931de718 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt @@ -1,31 +1,59 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app import android.app.Application +import android.os.Build +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import com.appmattus.certificatetransparency.installCertificateTransparencyProvider import de.gematik.ti.erp.app.core.AppScopedCache import de.gematik.ti.erp.app.di.ApplicationModule +import de.gematik.ti.erp.app.medicationplan.worker.createNotificationReminderChannel +import de.gematik.ti.erp.app.medicationplan.worker.createNotificationReminderGroup +import de.gematik.ti.erp.app.medicationplan.worker.scheduleReminderWorker +import kotlin.time.Duration -open class ErezeptApp : Application() { +open class ErezeptApp : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() + installCertificateTransparencyProvider() applicationModule = ApplicationModule(this) + + createNotificationReminderGroup() + createNotificationReminderChannel() + scheduleReminderWorker(Duration.ZERO) + } + + @Suppress("MagicNumber") + override fun newImageLoader(): ImageLoader { + return ImageLoader(this) + .newBuilder() + .components { + when { + Build.VERSION.SDK_INT >= 28 -> add(ImageDecoderDecoder.Factory()) + else -> add(GifDecoder.Factory()) + } + } + .build() } companion object { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt index 3ecb476e..0a09bc1a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app @@ -23,11 +23,8 @@ import android.view.WindowManager import androidx.activity.compose.setContent import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -36,52 +33,50 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.compose.rememberNavController import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi -import com.google.accompanist.navigation.material.rememberBottomSheetNavigator +import de.gematik.ti.erp.app.app.ApplicationScaffold import de.gematik.ti.erp.app.appupdate.navigation.AppUpdateNavHost -import de.gematik.ti.erp.app.authentication.ui.ExternalAuthPrompt -import de.gematik.ti.erp.app.authentication.ui.HealthCardPrompt -import de.gematik.ti.erp.app.authentication.ui.SecureHardwarePrompt +import de.gematik.ti.erp.app.authentication.presentation.rememberBiometricAuthenticator +import de.gematik.ti.erp.app.authentication.ui.components.BiometricPrompt +import de.gematik.ti.erp.app.authentication.ui.components.ExternalAuthPrompt +import de.gematik.ti.erp.app.authentication.ui.components.HealthCardPrompt import de.gematik.ti.erp.app.base.BaseActivity import de.gematik.ti.erp.app.cardwall.mini.ui.rememberAuthenticator import de.gematik.ti.erp.app.core.AppContent import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.core.LocalAuthenticator +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator +import de.gematik.ti.erp.app.core.LocalBottomSheetNavigator +import de.gematik.ti.erp.app.core.LocalBottomSheetNavigatorSheetState import de.gematik.ti.erp.app.core.LocalDi import de.gematik.ti.erp.app.core.LocalIntentHandler -import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeEnded -import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeStarted +import de.gematik.ti.erp.app.core.LocalNavController +import de.gematik.ti.erp.app.core.LocalTimeZone import de.gematik.ti.erp.app.features.BuildConfig -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenNavigation -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.rememberAppController +import de.gematik.ti.erp.app.mainscreen.ui.ExternalAuthenticationUiHandler +import de.gematik.ti.erp.app.navigation.ErezeptNavigatorFactory.initNavigation import de.gematik.ti.erp.app.prescription.detail.presentation.SharePrescriptionHandler import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod -import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod.Authenticated -import de.gematik.ti.erp.app.userauthentication.ui.UserAuthenticationScreen import de.gematik.ti.erp.app.utils.compose.DebugOverlay import de.gematik.ti.erp.app.utils.compose.DialogHost import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone import org.kodein.di.compose.withDI import org.kodein.di.instance -class MainActivity : BaseActivity() { +open class MainActivity : BaseActivity() { @VisibleForTesting(otherwise = VisibleForTesting.NONE) // Only visible for testing, otherwise shows a warning val testWrapper: TestWrapper by instance() @@ -96,31 +91,28 @@ class MainActivity : BaseActivity() { @RestrictTo(RestrictTo.Scope.TESTS) val elementsUsedInTests: SnapshotStateMap = mutableStateMapOf() - @OptIn(ExperimentalMaterialNavigationApi::class) + @OptIn(ExperimentalMaterialNavigationApi::class, ExperimentalMaterialApi::class) @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - catchAllUnCaughtExceptions() - + catchAllUnCaughtExceptions(this) lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { intent?.let { - when (it.action) { - DemoModeStarted.name -> setAsDemoMode() - DemoModeEnded.name -> cancelDemoMode() - else -> { - cancelDemoMode() - intentHandler.propagateIntent(it) - } - } + intentHandler.propagateIntent(it) } } } + @Requirement( + "O.Arch_10#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Trigger for update check" + ) lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { checkAppUpdate() + updateInAppMessage() } } @@ -130,34 +122,35 @@ class MainActivity : BaseActivity() { val view = LocalView.current val isUpdateAvailable by getAppUpdateFlagUseCase.invoke().collectAsStateWithLifecycle() - LaunchedEffect(view) { - ViewCompat.setWindowInsetsAnimationCallback(view, null) - } + LaunchedEffect(view) { ViewCompat.setWindowInsetsAnimationCallback(view, null) } + + val (navHostController, bottomSheetNavigator, sheetState) = initNavigation() withDI(di) { CompositionLocalProvider( - LocalActivity provides this, + LocalTimeZone provides TimeZone.currentSystemDefault(), LocalDi provides di, + LocalActivity provides this, + LocalNavController provides navHostController, + LocalBottomSheetNavigator provides bottomSheetNavigator, + LocalBottomSheetNavigatorSheetState provides sheetState, + LocalBiometricAuthenticator provides rememberBiometricAuthenticator(), LocalAnalytics provides analytics, LocalIntentHandler provides intentHandler, LocalAuthenticator provides rememberAuthenticator(intentHandler) ) { val authenticator = LocalAuthenticator.current - val bottomSheetNavigator = rememberBottomSheetNavigator() - val navController = rememberNavController(bottomSheetNavigator) + AppContent { if (isUpdateAvailable) { - AppUpdateNavHost(navController) + AppUpdateNavHost(navHostController) } else { val profilesController = rememberProfileController() - val mainScreenController = rememberMainScreenController() - val screenshotsAllowed by mainScreenController.screenshotsState + val mainScreenController = rememberAppController() + val isScreenshotsAllowed by mainScreenController.screenshotsState + + toggleScreenshotsAllowed(isScreenshotsAllowed) - if (screenshotsAllowed) { - this.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else { - this.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } val authentication by produceState(null) { launch { authenticationModeAndMethod.distinctUntilChangedBy { it::class } @@ -172,35 +165,30 @@ class MainActivity : BaseActivity() { } } - val noDrawModifier = Modifier.graphicsLayer(alpha = 0f) val activeProfile by profilesController.getActiveProfileState() - val isSsoTokenValid = rememberSaveable(activeProfile, activeProfile.ssoTokenScope) { + val isSsoTokenValid = rememberSaveable( + activeProfile, + activeProfile.ssoTokenScope, + activeProfile.ssoTokenScope?.token + ) { activeProfile.isSSOTokenValid() } Box { - if (authentication !is Authenticated) { - Image( - painterResource(R.drawable.erp_logo), - null, - modifier = Modifier.align(Alignment.Center) - ) - } - DialogHost { - Box( - if (authentication is Authenticated) Modifier else noDrawModifier - ) { + Box { // show mini card wall only when we have a invalid sso token if (!isSsoTokenValid) { HealthCardPrompt(authenticator.authenticatorHealthCard) ExternalAuthPrompt(authenticator.authenticatorExternal) } - SecureHardwarePrompt(authenticator.authenticatorSecureElement) + BiometricPrompt(authenticator.authenticatorBiometric) + + ExternalAuthenticationUiHandler() - MainScreenNavigation( - bottomSheetNavigator = bottomSheetNavigator, - navController = navController + ApplicationScaffold( + authentication = authentication, + isDemoMode = isDemoMode() ) SharePrescriptionHandler( @@ -209,19 +197,11 @@ class MainActivity : BaseActivity() { ) } } - - DialogHost { - AnimatedVisibility( - visible = authentication is AuthenticationModeAndMethod.AuthenticationRequired, - enter = fadeIn(), - exit = fadeOut() - ) { - UserAuthenticationScreen() - } - } } } + @Suppress("RestrictedApi") if (BuildConfig.DEBUG && BuildKonfig.DEBUG_VISUAL_TEST_TAGS) { + @Suppress("RestrictedApi") DebugOverlay(elementsUsedInTests) } } @@ -230,3 +210,17 @@ class MainActivity : BaseActivity() { } } } + +@Requirement( + "O.Data_13#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Default settings are not allow screenshots", + codeLines = 8 +) +private fun MainActivity.toggleScreenshotsAllowed(isScreenshotsAllowed: Boolean = false) { + if (isScreenshotsAllowed) { + this.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + this.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt index d7132760..5c22dca4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt index b44f0d0c..6ef12b48 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/UncaughtException.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/UncaughtException.kt index 75b7c286..113c4565 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/UncaughtException.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/UncaughtException.kt @@ -1,30 +1,39 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app +import com.ensody.reactivestate.isFatal import org.jose4j.base64url.Base64Url -class UncaughtException(private val throwable: Throwable) : Throwable(cause = throwable) { +class UncaughtException( + private val throwable: Throwable, + private val packageName: String, + private val isDemoMode: Boolean +) : Throwable(cause = throwable) { override fun toString(): String { val name = throwable::class::simpleName.name val message = throwable.localizedMessage val trace = throwable.stackTraceToString() + val className = throwable.stackTrace[0].className + val methodName = throwable.stackTrace[0].methodName + val fileName = throwable.stackTrace[0].fileName + val lineNumber = throwable.stackTrace[0].lineNumber return when { message != null -> { @@ -32,16 +41,26 @@ class UncaughtException(private val throwable: Throwable) : Throwable(cause = th .encodeUtf8ByteRepresentation(message) .replace('-', '$') // class names don't contain any minus symbol - "${name}_$msgBase64: \nlocalizedMessage: $message\n stackTrace: $trace" + "${name}_$msgBase64: \n" + + "localizedMessage: $message\n " + + "activity-info: package-$packageName is-demo-mode-$isDemoMode\n " + + "className: $className\n" + + "methodName: $methodName\n" + + "fileName: $fileName\n" + + "lineNumber: $lineNumber\n" + + "stackTrace: $trace\n" + + "isFatal: ${throwable.isFatal()}" } - else -> name - } - } -} -fun catchUncaughtException() { - val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() - Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - defaultExceptionHandler?.uncaughtException(thread, UncaughtException(throwable)) + else -> + "$name (no message)\n " + + "activity-info: package-$packageName is-demo-mode-$isDemoMode\n " + + "className: $className\n" + + "methodName: $methodName\n" + + "fileName: $fileName\n" + + "lineNumber: $lineNumber\n" + + "stackTrace: ${trace}\n" + + "isFatal: ${throwable.isFatal()}" + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt index b183a3b3..81d34152 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt index 00cb7c45..41dd3604 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt @@ -1,47 +1,40 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics -import android.content.Context -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController import com.contentsquare.android.Contentsquare import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCaseData import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase +import de.gematik.ti.erp.app.analytics.usecase.StartTrackerUseCase +import de.gematik.ti.erp.app.analytics.usecase.StopTrackerUseCase +import de.gematik.ti.erp.app.base.BaseActivity import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenBottomSheetContentState -import de.gematik.ti.erp.app.pharmacy.ui.PharmacySearchSheetContentState -import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailBottomSheetContent import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -50,15 +43,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -@Requirement( - "A_19095", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Resets the analytics id and creates a new one [-> init block]." -) class Analytics( - private val context: Context, private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + private val startTrackerUseCase: StartTrackerUseCase, + private val stopTrackerUseCase: StopTrackerUseCase, private val analyticsUseCase: AnalyticsUseCase, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { @@ -73,33 +62,37 @@ class Analytics( get() = isAnalyticsAllowed.stateIn(scope, SharingStarted.Eagerly, false) @Requirement( - "A_19093", - "A_19094", + "A_19093-01#1", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Only screen names and data like error states are transmitted." + rationale = " ...track screens for analytics purposes." ) @Requirement( - "O.Purp_2#5", + "O.Purp_2#4", "O.Purp_4#1", - "O.Data_6#5", + "O.Data_6#6", sourceSpecification = "BSI-eRp-ePA", - rationale = "User interaction analytics trigger records data only if user opt-in is given." + rationale = "...recording for screens here too after opt-in", + codeLines = 8 ) fun trackScreen(screenName: String) { if (analyticsAllowed.value) { Contentsquare.send(screenName) Napier.d("Analytics send $screenName") + } else { + Napier.d("Analytics not allowed") } } - init { + /** + * This init function is called from the [BaseActivity] to initialize the analytics. + */ + fun init(activity: BaseActivity) { Napier.d("Init Analytics") - - Contentsquare.forgetMe() - scope.launch { val isAllowed = isAnalyticsAllowed.first() - setAnalyticsPreference(isAllowed) + activity.runOnUiThread { + setAnalyticsPreference(isAllowed) + } } } @@ -116,23 +109,6 @@ class Analytics( @Composable get() = analyticsScreenFlow.collectAsStateWithLifecycle(AnalyticsData.defaultAnalyticsState) - fun onPopUpShown(popUpScreenName: String) { - if (analyticsAllowed.value) { - popUpFlow.value = AnalyticsData.PopUp(true, popUpScreenName) - } - } - - fun onPopUpClosed() { - if (analyticsAllowed.value) { - popUpFlow.value = AnalyticsData.PopUp(false, "") - } - } - - @Requirement( - "A_20187#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Enable analytics" - ) @Requirement( "O.Purp_5#5", sourceSpecification = "BSI-eRp-ePA", @@ -144,32 +120,31 @@ class Analytics( else -> disallowAnalytics() } } + private fun allowAnalytics() { scope.launch { changeAnalyticsStateUseCase.invoke(true) + startTrackerUseCase() } - Contentsquare.optIn(context) - Napier.i("Analytics allowed") } @Requirement( - "A_20187#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Disable analytics" - ) - @Requirement( - "O.Purp_5#6", + "O.Purp_5#7", sourceSpecification = "BSI-eRp-ePA", rationale = "Disable usage analytics." ) private fun disallowAnalytics() { scope.launch { changeAnalyticsStateUseCase.invoke(false) + stopTrackerUseCase() } - Contentsquare.optOut(context) - Napier.i("Analytics disallowed") } + @Requirement( + "A_19093-01#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = " ...track user is authenticated." + ) fun trackIdentifiedWithIDP() { trackScreen("idp_authenticated") } @@ -186,72 +161,16 @@ class Analytics( UserNotAuthenticated("user_not_authenticated") } + @Requirement( + "A_19093-01#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = " ...track user has authentication error." + ) fun trackAuthenticationProblem(kind: AuthenticationProblem) { trackScreen("auth_error_${kind.event}") } } -@Suppress("ComposableNaming") -@Composable -fun trackNavigationChangesAsync( - navController: NavHostController, - previousNavEntry: String, - onNavEntryChange: (String) -> Unit -) { - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - - LaunchedEffect(navController.currentBackStackEntry) { - async { - try { - val route = Uri.parse(navController.currentBackStackEntry?.destination?.route) - .buildUpon().clearQuery().build().toString() - if (route != previousNavEntry) { - onNavEntryChange(route) - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } catch (expected: Exception) { - Napier.e("Couldn't track navigation screen", expected) - } - } - } -} - -fun trackScreenUsingNavEntry( - route: String, - analytics: Analytics, - analyticsList: List -) { - try { - val name = analyticsList.find { it.key == route }?.name ?: "" - val trackedName = when { - name.isNotEmpty() -> name - else -> route - } - Napier.d { "Content square tracking: Key is $trackedName" } - analytics.trackScreen(trackedName) - } catch (expected: Exception) { - Napier.e("Couldn't track navigation screen", expected) - } -} - -@Suppress("ComposableNaming") -@Composable -fun trackPopUps( - analytics: Analytics, - analyticsState: AnalyticsData.AnalyticsScreenState -) { - LaunchedEffect(analyticsState) { - if (analyticsState.popUp.visible) { - analytics.trackScreen( - analyticsState.screenNamesList.find { - it.key == analyticsState.popUp.name - }?.name ?: "" - ) - } - } -} - fun Analytics.trackAuth(state: AuthenticationState) { if (analyticsAllowed.value) { when (state) { @@ -288,100 +207,6 @@ fun Analytics.trackAuth(state: AuthenticationState) { } } -fun Analytics.trackPrescriptionDetailPopUps(content: PrescriptionDetailBottomSheetContent) { - if (analyticsAllowed.value) { - when (content) { - is PrescriptionDetailBottomSheetContent.HowLongValid -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.SubstitutionAllowed -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.DirectAssignment -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.EmergencyFee -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.AdditionalFeeExempt -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.Scanned -> { - onPopUpShown(content.popUp.name) - } - - is PrescriptionDetailBottomSheetContent.Failure -> { - onPopUpShown(content.popUp.name) - } - } - } -} - -fun Analytics.trackMainScreenBottomPopUps(content: MainScreenBottomSheetContentState) { - if (analyticsAllowed.value) { - when (content) { - is MainScreenBottomSheetContentState.AddProfile -> { - onPopUpShown(content.popUp.name) - } - - is MainScreenBottomSheetContentState.Welcome -> { - onPopUpShown(content.popUp.name) - } - - is MainScreenBottomSheetContentState.EditProfileName -> { - onPopUpShown(content.popUp.name) - } - - is MainScreenBottomSheetContentState.EditProfilePicture -> { - onPopUpShown(content.popUp.name) - } - - is MainScreenBottomSheetContentState.GrantConsent -> { - onPopUpShown(content.popUp.name) - } - } - } -} - -fun Analytics.trackPharmacySearchPopUps(content: PharmacySearchSheetContentState) { - if (analyticsAllowed.value) { - when (content) { - is PharmacySearchSheetContentState.PharmacySelected -> { - onPopUpShown(content.popUp.name) - } - - is PharmacySearchSheetContentState.FilterSelected -> { - onPopUpShown(content.popUp.name) - } - } - } -} - -fun Analytics.trackScannerPopUps() { - if (analyticsAllowed.value) { - onPopUpShown("main_scanner_successDrawer") - } -} - -fun Analytics.trackOrderPopUps() { - if (analyticsAllowed.value) { - onPopUpShown("orders_pickupCode") - } -} - object AnalyticsData { @Immutable data class AnalyticsScreenState( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/PopUpName.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/PopUpName.kt index 31a0008c..1a6154a6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/PopUpName.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/PopUpName.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/di/AnalyticsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/di/AnalyticsModule.kt index c6c3a74f..bdeebd70 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/di/AnalyticsModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/di/AnalyticsModule.kt @@ -1,34 +1,36 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.di import de.gematik.ti.erp.app.analytics.Analytics -import de.gematik.ti.erp.app.analytics.mapper.ContentSquareMapper -import de.gematik.ti.erp.app.analytics.presentation.DemoTrackerScreenViewModel +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareScreenMapper +import de.gematik.ti.erp.app.analytics.presentation.DebugTrackerScreenViewModel import de.gematik.ti.erp.app.analytics.tracker.ContentSquareTracker -import de.gematik.ti.erp.app.analytics.tracker.DemoTracker -import de.gematik.ti.erp.app.analytics.tracker.DemoTrackerSession +import de.gematik.ti.erp.app.analytics.tracker.DebugTracker +import de.gematik.ti.erp.app.analytics.tracker.DebugTrackerSession import de.gematik.ti.erp.app.analytics.tracker.Tracker import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase -import de.gematik.ti.erp.app.analytics.usecase.GetDemoTrackingSessionUseCase +import de.gematik.ti.erp.app.analytics.usecase.GetDebugTrackingSessionUseCase import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase +import de.gematik.ti.erp.app.analytics.usecase.StartTrackerUseCase +import de.gematik.ti.erp.app.analytics.usecase.StopTrackerUseCase import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.bindSingleton @@ -38,14 +40,16 @@ val analyticsModule = DI.Module("analyticsModule") { bindProvider { IsAnalyticsAllowedUseCase(instance()) } bindProvider { ChangeAnalyticsStateUseCase(instance()) } bindProvider { AnalyticsUseCase(instance()) } + bindProvider { StartTrackerUseCase(instance()) } + bindProvider { StopTrackerUseCase(instance()) } - bindSingleton { Analytics(instance(), instance(), instance(), instance()) } - bindSingleton { DemoTrackerSession() } + bindSingleton { Analytics(instance(), instance(), instance(), instance(), instance()) } + bindSingleton { DebugTrackerSession() } - bindProvider { ContentSquareMapper() } + bindProvider { ContentSquareScreenMapper() } + bindProvider { Tracker(instance(), instance(), instance()) } bindProvider { ContentSquareTracker() } - bindProvider { DemoTracker(instance()) } - bindProvider { Tracker(instance(), instance()) } - bindProvider { GetDemoTrackingSessionUseCase(instance()) } - bindProvider { DemoTrackerScreenViewModel(instance()) } + bindProvider { DebugTracker(instance()) } + bindProvider { GetDebugTrackingSessionUseCase(instance()) } + bindProvider { DebugTrackerScreenViewModel(instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareEventMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareEventMapper.kt new file mode 100644 index 00000000..399d7eaf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareEventMapper.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.mapper + +enum class ContentSquareEventMapper(val trackingParameter: String) { + ArchivePrescriptionCount("archive_prescription_count"), + SyncedPrescriptionCount("synced_prescription_count"), + ScannedPrescriptionCount("scanned_prescription_count"), + MessageCount("messages_count"); +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt deleted file mode 100644 index 3969f194..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.mapper - -import de.gematik.ti.erp.app.navigation.NavigationRouteNames - -class ContentSquareMapper { - @Suppress("ComplexMethod") - fun map(routeNames: NavigationRouteNames): String? = - when (routeNames) { - NavigationRouteNames.DeviceCheckLoadingScreen -> "appsecurity:loading" - NavigationRouteNames.InsecureDeviceScreen -> "main:deviceSecurity" - NavigationRouteNames.IntegrityWarningScreen -> "main:integrityWarning" - - NavigationRouteNames.PrescriptionDetailScreen -> "prescriptionDetail" - NavigationRouteNames.PrescriptionDetailMedicationScreen -> "prescriptionDetail:medication" - NavigationRouteNames.PrescriptionDetailPatientScreen -> "prescriptionDetail:patient" - NavigationRouteNames.PrescriptionDetailPractitionerScreen -> "prescriptionDetail:practitioner" - NavigationRouteNames.PrescriptionDetailOrganizationScreen -> "prescriptionDetail:organization" - NavigationRouteNames.PrescriptionDetailMedicationOverviewScreen -> "prescriptionDetail:medicationOverview" - NavigationRouteNames.PrescriptionDetailMedicationIngredientsScreen -> - "prescriptionDetail:medication_ingredients" - - NavigationRouteNames.PrescriptionDetailAccidentInfoScreen -> "prescriptionDetail:accidentInfo" - NavigationRouteNames.PrescriptionDetailTechnicalInfoScreen -> "prescriptionDetail:technicalInfo" - - NavigationRouteNames.ProfileScreen -> "profile" - NavigationRouteNames.ProfileEditPictureScreen -> "profile:editPicture" - NavigationRouteNames.ProfileImageCropperScreen -> "profile:editPicture:imageCropper" - NavigationRouteNames.ProfileTokenScreen -> "profile:token" - NavigationRouteNames.ProfileAuditEventsScreen -> "profile:auditEvents" - NavigationRouteNames.ProfilePairedDevicesScreen -> "profile:registeredDevices" - - NavigationRouteNames.InvoiceListScreen -> "chargeItem:list" - NavigationRouteNames.InvoiceDetailsScreen -> "chargeItem:details" - NavigationRouteNames.InvoiceExpandedDetailsScreen -> "chargeItem:details:expanded" - NavigationRouteNames.InvoiceLocalCorrectionScreen -> "chargeItem:localCorrection" - NavigationRouteNames.InvoiceShareScreen -> "chargeItem:share" - - NavigationRouteNames.OnboardingWelcomeScreen -> "onboarding:welcome" - NavigationRouteNames.AllowAnalyticsScreen -> "onboarding:allowAnalytics" - NavigationRouteNames.BiometricScreen -> "onboarding:biometric" - NavigationRouteNames.TermsOfUseScreen -> "onboarding:termsOfUse" - NavigationRouteNames.DataProtectionScreen -> "onboarding:dataProtection" - NavigationRouteNames.OnboardingSelectAppLoginScreen -> "onboarding:selectAppLogin" - NavigationRouteNames.OnboardingDataProtectionAndTermsOfUseOverviewScreen -> - "onboarding:termsOfUseAndDataProtection" - - NavigationRouteNames.OnboardingAnalyticsPreviewScreen -> "onboarding:analyticsPreview" - - NavigationRouteNames.MlKit -> "mlKit" - NavigationRouteNames.MlKitInformationScreen -> "mlKit:information" - - NavigationRouteNames.CardUnlockIntroScreen -> "healthCardPassword:introduction" - NavigationRouteNames.CardUnlockCanScreen -> "healthCardPassword:can" - NavigationRouteNames.CardUnlockPukScreen -> "healthCardPassword:puk" - NavigationRouteNames.CardUnlockOldSecretScreen -> "healthCardPassword:oldPin" - NavigationRouteNames.CardUnlockNewSecretScreen -> "healthCardPassword:pin" - NavigationRouteNames.CardUnlockEgkScreen -> "healthCardPassword:readCard" - - NavigationRouteNames.CardWallIntroScreen -> "cardWall:welcome" - NavigationRouteNames.CardWallCanScreen -> "cardWall:CAN" - NavigationRouteNames.CardWallPinScreen -> "cardWall:PIN" - NavigationRouteNames.CardWallSaveCredentialsScreen -> "cardWall:saveCredentials:initial" - NavigationRouteNames.CardWallSaveCredentialsInfoScreen -> "cardWall:saveCredentials:information" - NavigationRouteNames.CardWallReadCardScreen -> "cardWall:connect" - NavigationRouteNames.CardWallExternalAuthenticationScreen -> "cardWall:extAuth" - - NavigationRouteNames.TroubleShootingIntroScreen -> "troubleShooting" - NavigationRouteNames.TroubleShootingDeviceOnTopScreen -> "troubleShooting:readCardHelp1" - NavigationRouteNames.TroubleShootingFindNfcPositionScreen -> "troubleShooting:readCardHelp2" - NavigationRouteNames.TroubleShootingNoSuccessScreen -> "troubleShooting:readCardHelp3" - - NavigationRouteNames.SettingsScreen -> "settings" - NavigationRouteNames.SettingsAccessibilityScreen -> "settings:accessibility" - NavigationRouteNames.SettingsProductImprovementScreen -> "settings:productImprovements" - NavigationRouteNames.SettingsAllowAnalyticsScreen -> "settings:productImprovements:complyTracking" - NavigationRouteNames.SettingsDeviceSecurityScreen -> "settings:authenticationMethods" - NavigationRouteNames.SettingsSetAppPasswordScreen -> "settings:authenticationMethods:setAppPassword" - NavigationRouteNames.SettingsDataProtectionScreen -> "settings:dataProtection" - NavigationRouteNames.SettingsTermsOfUseScreen -> "settings:termsOfUse" - NavigationRouteNames.SettingsLegalNoticeScreen -> "settings:legalNotice" - NavigationRouteNames.SettingsOpenSourceLicencesScreen -> "settings:openSourceLicence" - NavigationRouteNames.SettingsAdditionalLicencesScreen -> "settings:additionalLicence" - - NavigationRouteNames.PrescriptionScanScreen -> "main:scanner" - - NavigationRouteNames.PharmacyStartScreen -> "pharmacySearch" - NavigationRouteNames.PharmacyFilterSheetScreen -> "pharmacySearch:filter" - NavigationRouteNames.PharmacySearchListScreen -> "pharmacySearch:detail" - NavigationRouteNames.PharmacySearchMapsScreen -> "pharmacySearch:map" - NavigationRouteNames.PharmacyOrderOverviewScreen -> "redeem:viaTI" - NavigationRouteNames.PharmacyEditShippingContactScreen -> "redeem:editContactInformation" - NavigationRouteNames.PharmacyPrescriptionSelectionScreen -> "redeem:prescriptionChooseSubset" - - NavigationRouteNames.SampleOverviewScreen -> null - NavigationRouteNames.BottomSheetSampleScreen -> null - NavigationRouteNames.BottomSheetSampleLargeScreen -> null - NavigationRouteNames.BottomSheetSampleSmallScreen -> null - NavigationRouteNames.DemoTrackerScreen -> null - - NavigationRouteNames.AppUpdateScreen -> "appUpdate" - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareScreenMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareScreenMapper.kt new file mode 100644 index 00000000..287c892f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareScreenMapper.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.mapper + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.navigation.NavigationRouteNames + +@Requirement( + "A_19094-01#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Every screen name has its own tracking string which is statically typed and only that is sent." +) +class ContentSquareScreenMapper { + @Suppress("ComplexMethod") + fun map(routeName: NavigationRouteNames): String? = + if (routeName.doNoTrack) { + null + } else { + when (routeName) { + NavigationRouteNames.DeviceCheckLoadingScreen -> null + NavigationRouteNames.InsecureDeviceScreen -> "main:deviceSecurity" + NavigationRouteNames.IntegrityWarningScreen -> "main:integrityWarning" + + NavigationRouteNames.PrescriptionDetailScreen -> "prescriptionDetail" + NavigationRouteNames.PrescriptionDetailMedicationScreen -> "prescriptionDetail:medication" + NavigationRouteNames.PrescriptionDetailPatientScreen -> "prescriptionDetail:patient" + NavigationRouteNames.PrescriptionDetailPractitionerScreen -> "prescriptionDetail:practitioner" + NavigationRouteNames.PrescriptionDetailOrganizationScreen -> "prescriptionDetail:organization" + NavigationRouteNames.PrescriptionDetailMedicationOverviewScreen -> "prescriptionDetail:medicationOverview" + NavigationRouteNames.PrescriptionDetailMedicationIngredientsScreen -> + "prescriptionDetail:medication_ingredients" + + NavigationRouteNames.PrescriptionDetailAccidentInfoScreen -> "prescriptionDetail:accidentInfo" + NavigationRouteNames.PrescriptionDetailTechnicalInfoScreen -> "prescriptionDetail:technicalInfo" + + NavigationRouteNames.PrescriptionDetailSelfPayerPrescriptionBottomSheetScreen -> + "prescriptionDetail:selfPayerPrescriptionBottomSheet" + + NavigationRouteNames.PrescriptionDetailAdditionalFeeNotExemptBottomSheetScreen -> + "prescriptionDetail:additionalFeeNotExemptBottomSheet" + + NavigationRouteNames.PrescriptionDetailAdditionalFeeExemptBottomSheetScreen -> + "prescriptionDetail:additionalFeeExemptBottomSheet" + + NavigationRouteNames.PrescriptionDetailFailureBottomSheetScreen -> "prescriptionDetail:failureBottomSheet" + NavigationRouteNames.PrescriptionDetailScannedBottomSheetScreen -> "prescriptionDetail:scannedBottomSheet" + NavigationRouteNames.PrescriptionDetailDirectAssignmentBottomSheetScreen -> "prescriptionDetail:directAssignmentBottomSheet" + NavigationRouteNames.PrescriptionDetailSubstitutionAllowedBottomSheetScreen -> + "prescriptionDetail:substitutionAllowedBottomSheet" + + NavigationRouteNames.PrescriptionDetailSubstitutionNotAllowedBottomSheetScreen -> + "prescriptionDetail:substitutionNotAllowedBottomSheet" + + NavigationRouteNames.PrescriptionDetailEmergencyFeeExemptBottomSheetScreen -> + "prescriptionDetail:emergencyFeeExemptBottomSheet" + + NavigationRouteNames.PrescriptionDetailEmergencyFeeNotExemptBottomSheetScreen -> + "prescriptionDetail:emergencyFeeNotExemptBottomSheet" + + NavigationRouteNames.PrescriptionDetailHowLongValidBottomSheetScreen -> "prescriptionDetail:howLongValidBottomSheet" + + NavigationRouteNames.ProfileScreen -> "profile" + NavigationRouteNames.ProfileEditPictureScreen -> "profile:editPicture:fullscreen" + NavigationRouteNames.ProfileImageCropperScreen -> "profile:editPicture:imageCropper" + NavigationRouteNames.ProfileImageEmojiScreen -> "profile:editPicture:imageEmoji" + NavigationRouteNames.ProfileImageCameraScreen -> "profile:editPicture:imageCamera" + NavigationRouteNames.ProfileAuditEventsScreen -> "profile:auditEvents" + NavigationRouteNames.ProfilePairedDevicesScreen -> "profile:registeredDevices" + NavigationRouteNames.ProfileEditPictureBottomSheetScreen -> "main:editProfilePicture" + NavigationRouteNames.ProfileEditNameBottomSheetScreen -> "main:editName" + NavigationRouteNames.ProfileAddNameBottomSheetScreen -> "main:createProfile" + + NavigationRouteNames.InvoiceListScreen -> "chargeItem:list" + NavigationRouteNames.InvoiceDetailsScreen -> "chargeItem:details" + NavigationRouteNames.InvoiceExpandedDetailsScreen -> "chargeItem:details:expanded" + NavigationRouteNames.InvoiceLocalCorrectionScreen -> "chargeItem:localCorrection" + NavigationRouteNames.InvoiceShareScreen -> "chargeItem:share" + + NavigationRouteNames.OnboardingWelcomeScreen -> "onboarding:welcome" + NavigationRouteNames.AllowAnalyticsScreen -> "onboarding:allowAnalytics" + NavigationRouteNames.BiometricScreen -> "onboarding:biometric" + NavigationRouteNames.TermsOfUseScreen -> "onboarding:termsOfUse" + NavigationRouteNames.DataProtectionScreen -> "onboarding:dataProtection" + NavigationRouteNames.OnboardingSelectAppLoginScreen -> "onboarding:selectAppLogin" + NavigationRouteNames.OnboardingDataProtectionAndTermsOfUseOverviewScreen -> + "onboarding:termsOfUseAndDataProtection" + + NavigationRouteNames.OnboardingAnalyticsPreviewScreen -> "onboarding:analyticsPreview" + + NavigationRouteNames.MlKit -> "mlKit:intro" + NavigationRouteNames.MlKitInformationScreen -> "mlKit:information" + + NavigationRouteNames.CardUnlockIntroScreen -> "healthCardPassword:introduction" + NavigationRouteNames.CardUnlockCanScreen -> "healthCardPassword:can" + NavigationRouteNames.CardUnlockPukScreen -> "healthCardPassword:puk" + NavigationRouteNames.CardUnlockOldSecretScreen -> "healthCardPassword:oldPin" + NavigationRouteNames.CardUnlockNewSecretScreen -> "healthCardPassword:pin" + NavigationRouteNames.CardUnlockEgkScreen -> "healthCardPassword:readCard" + + NavigationRouteNames.CardWallIntroScreen -> "cardWall:welcome" + NavigationRouteNames.CardWallCanScreen -> "cardWall:CAN" + NavigationRouteNames.CardWallPinScreen -> "cardWall:PIN" + NavigationRouteNames.CardWallSaveCredentialsScreen -> "cardWall:saveCredentials:initial" + NavigationRouteNames.CardWallSaveCredentialsInfoScreen -> "cardWall:saveCredentials:information" + NavigationRouteNames.CardWallReadCardScreen -> "cardWall:connect" + NavigationRouteNames.CardWallGidListScreen -> "cardWall:extAuth" + NavigationRouteNames.CardWallGidHelpScreen -> "cardWall:extAuth:help" + + NavigationRouteNames.TroubleShootingIntroScreen -> "troubleShooting" + NavigationRouteNames.TroubleShootingDeviceOnTopScreen -> "troubleShooting:readCardHelp1" + NavigationRouteNames.TroubleShootingFindNfcPositionScreen -> "troubleShooting:readCardHelp2" + NavigationRouteNames.TroubleShootingNoSuccessScreen -> "troubleShooting:readCardHelp3" + + NavigationRouteNames.SettingsScreen -> "settings" + NavigationRouteNames.SettingsProductImprovementScreen -> "settings:productImprovements" + NavigationRouteNames.SettingsAllowAnalyticsScreen -> "settings:productImprovements:complyTracking" + NavigationRouteNames.SettingsAppSecurityScreen -> "settings:authenticationMethods" + NavigationRouteNames.SettingsSetAppPasswordScreen -> "settings:authenticationMethods:setAppPassword" + NavigationRouteNames.SettingsDataProtectionScreen -> "settings:dataProtection" + NavigationRouteNames.SettingsTermsOfUseScreen -> "settings:termsOfUse" + NavigationRouteNames.SettingsLegalNoticeScreen -> "settings:legalNotice" + NavigationRouteNames.SettingsOpenSourceLicencesScreen -> "settings:openSourceLicence" + NavigationRouteNames.SettingsAdditionalLicencesScreen -> "settings:additionalLicence" + NavigationRouteNames.SettingsLanguageScreen -> "settings:language" + + NavigationRouteNames.PrescriptionsScreen -> "main" + NavigationRouteNames.PrescriptionsArchiveScreen -> "main:prescriptionArchive" + NavigationRouteNames.PrescriptionScanScreen -> "main:scanner" + NavigationRouteNames.WelcomeDrawerBottomSheetScreen -> "main:welcomeDrawer" + NavigationRouteNames.GrantConsentBottomSheetScreen -> "main:grantConsent" + + NavigationRouteNames.MessageListScreen -> "orders" + NavigationRouteNames.MessageDetailScreen -> "orders:details" + NavigationRouteNames.MessageBottomSheetScreen -> "orders:details:reply" + NavigationRouteNames.PharmacyDetailsFromMessageScreen -> "orders:details:selectedPharmacy" + + NavigationRouteNames.PharmacyStartScreen -> "pharmacySearch" + NavigationRouteNames.PharmacyStartScreenModal -> "pharmacySearch" + NavigationRouteNames.PharmacyFilterSheetScreen -> "pharmacySearch:filter" + NavigationRouteNames.PharmacySearchListScreen -> "pharmacySearch:detail" + NavigationRouteNames.PharmacySearchMapsScreen -> "pharmacySearch:map" + NavigationRouteNames.PharmacyDetailsFromPharmacyScreen -> "pharmacySearch:selectedPharmacy" + + NavigationRouteNames.BottomSheetShowcaseScreen -> null + NavigationRouteNames.DemoTrackerScreen -> null + + NavigationRouteNames.AppUpdateScreen -> "appUpdate" + NavigationRouteNames.RedeemMethodSelection -> "redeem:methodSelection" + NavigationRouteNames.RedeemPrescriptionSelection -> "redeem:prescriptionChooseSubset" + NavigationRouteNames.RedeemLocal -> "redeem:matrixcode" + NavigationRouteNames.RedeemOnline -> "redeem:prescriptionAllOrSelection" + NavigationRouteNames.SuccessScreen -> "medicationplan:success" + NavigationRouteNames.DosageInfoScreen -> "medicationplan:dosageInfo" + NavigationRouteNames.ScheduleListScreen -> "medicationplan:scheduleList" + NavigationRouteNames.ScheduleScreen -> "medicationplan:schedule" + NavigationRouteNames.ScheduleDateRangeScreen -> "medicationplan:scheduleDateRange" + NavigationRouteNames.RedeemOrderOverviewScreen -> "redeem:viaTI" + NavigationRouteNames.RedeemEditShippingContactScreen -> "redeem:editContactInformation" + NavigationRouteNames.RedeemPrescriptionSelectionScreen -> "redeem:prescriptionChooseSubset" + + NavigationRouteNames.OrderHealthCardSelectInsuranceCompanyScreen -> "contactInsuranceCompany" + NavigationRouteNames.OrderHealthCardSelectOptionScreen -> "contactInsuranceCompany:selectReason" + NavigationRouteNames.OrderHealthCardSelectMethodScreen -> "contactInsuranceCompany:selectMethod" + + NavigationRouteNames.UserAuthenticationScreen -> "userAuthentication" + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/model/TrackingModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/model/TrackingModels.kt deleted file mode 100644 index ef51f863..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/model/TrackingModels.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.model - -data class TrackedEvent( - val key: String?, - val value: String -) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreenRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreenRoutes.kt index 0973eb3f..36d3f1cb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreenRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreenRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreensGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreensGraph.kt index ed34a516..ee9216be 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreensGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/navigation/TrackingScreensGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.navigation @@ -21,7 +21,7 @@ package de.gematik.ti.erp.app.analytics.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation -import de.gematik.ti.erp.app.analytics.ui.DemoTrackerScreen +import de.gematik.ti.erp.app.analytics.ui.DebugTrackerScreen import de.gematik.ti.erp.app.navigation.renderComposable import de.gematik.ti.erp.app.navigation.slideInDown import de.gematik.ti.erp.app.navigation.slideOutUp @@ -38,7 +38,7 @@ fun NavGraphBuilder.trackingGraph( stackEnterAnimation = { slideInDown() }, stackExitAnimation = { slideOutUp() } ) { - DemoTrackerScreen( + DebugTrackerScreen( navController, it ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DebugTrackerScreenViewModel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DebugTrackerScreenViewModel.kt new file mode 100644 index 00000000..b75b8217 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DebugTrackerScreenViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ensody.reactivestate.derived +import com.ensody.reactivestate.get +import de.gematik.ti.erp.app.analytics.tracker.EVENT_TRACKED +import de.gematik.ti.erp.app.analytics.usecase.GetDebugTrackingSessionUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +sealed interface DebugTrackerScreenEvent { + class ScreenEvent(val value: String) : DebugTrackerScreenEvent + class DynamicEvent(val key: String, val value: String) : DebugTrackerScreenEvent +} + +@Suppress("MagicNumber") +class DebugTrackerScreenViewModel( + private val getDebugTrackingSessionUseCase: GetDebugTrackingSessionUseCase +) : ViewModel() { + + private val _session = MutableStateFlow>(emptyList()) + + init { + viewModelScope.launch { + _session.value = getDebugTrackingSessionUseCase.invoke().map { trackedValue -> + if (trackedValue.contains(EVENT_TRACKED)) { + val splits = trackedValue.split(";") + if (splits.size == 3) { + DebugTrackerScreenEvent.DynamicEvent(splits[1], splits[2]) + } else { + DebugTrackerScreenEvent.DynamicEvent("unknown", "unknown") + } + } else { + DebugTrackerScreenEvent.ScreenEvent(trackedValue) + } + } + } + } + + val session: StateFlow> = derived { get(_session) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DemoTrackerScreenViewModel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DemoTrackerScreenViewModel.kt deleted file mode 100644 index be683eba..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/presentation/DemoTrackerScreenViewModel.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.presentation - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ensody.reactivestate.derived -import com.ensody.reactivestate.get -import de.gematik.ti.erp.app.analytics.usecase.GetDemoTrackingSessionUseCase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch - -class DemoTrackerScreenViewModel( - private val getDemoTrackingSessionUseCase: GetDemoTrackingSessionUseCase -) : ViewModel() { - - private val mutableSession = MutableStateFlow>(emptyList()) - init { - viewModelScope.launch { - mutableSession.value = getDemoTrackingSessionUseCase.invoke() - } - } - - val session: StateFlow> = derived { get(mutableSession) } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/ContentSquareTracker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/ContentSquareTracker.kt index d386e406..1d583e87 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/ContentSquareTracker.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/ContentSquareTracker.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.tracker import com.contentsquare.android.Contentsquare +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareEventMapper import io.github.aakira.napier.Napier class ContentSquareTracker { @@ -29,4 +30,11 @@ class ContentSquareTracker { Napier.i { "Contentsquare send screenName=$screenName" } Contentsquare.send(screenName) } + + fun send( + event: Pair + ) { + Napier.i { "Contentsquare send eventName=${event.first.trackingParameter} | ${event.second}" } + Contentsquare.send(event.first.trackingParameter, event.second) + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTracker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTracker.kt new file mode 100644 index 00000000..c26e8246 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTracker.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.tracker + +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareEventMapper +import io.github.aakira.napier.Napier + +const val EVENT_TRACKED = "EventTracked" + +class DebugTracker( + private val session: DebugTrackerSession +) { + fun track( + screenName: String + ) { + session.addScreen(screenName) + } + + fun send( + event: Pair + ) { + Napier.i { "Debug tracker send eventName=${event.first.trackingParameter} | ${event.second}" } + session.addScreen("$EVENT_TRACKED;${event.first.trackingParameter};${event.second}") + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTrackerSession.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTrackerSession.kt new file mode 100644 index 00000000..83a7d981 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DebugTrackerSession.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.tracker + +class DebugTrackerSession { + + private val screens = mutableListOf() + + fun getScreens() = screens.toList() + + fun addScreen(screen: String) = screens.add(screen) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTracker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTracker.kt deleted file mode 100644 index 24182b9f..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTracker.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.tracker - -class DemoTracker( - private val session: DemoTrackerSession -) { - fun track( - screenName: String - ) { - session.addScreen(screenName) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTrackerSession.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTrackerSession.kt deleted file mode 100644 index 216b1474..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/DemoTrackerSession.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.tracker - -class DemoTrackerSession { - - private val screens = mutableListOf() - - fun getScreens() = screens.toList() - - fun addScreen(screen: String) = screens.add(screen) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/Tracker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/Tracker.kt index 1cbae696..b804a8f3 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/Tracker.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/tracker/Tracker.kt @@ -1,73 +1,108 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.tracker import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.NonRestartableComposable -import de.gematik.ti.erp.app.analytics.mapper.ContentSquareMapper +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareScreenMapper +import de.gematik.ti.erp.app.analytics.model.TrackedEvent import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase import de.gematik.ti.erp.app.navigation.NavigationRouteNames import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext import org.kodein.di.compose.rememberInstance class Tracker( + private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, private val contentSquareTracker: ContentSquareTracker, - private val demoTracker: DemoTracker + private val debugTracker: DebugTracker, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - @NonRestartableComposable @Composable + @Suppress("ComposableNaming") fun computeScreenTrackingProperty( - routeEnum: String, - block: (String) -> Unit - ) { - val mapper: ContentSquareMapper by rememberInstance() - val isAnalyticsAllowed: IsAnalyticsAllowedUseCase by rememberInstance() - - val screenName = mapper.map(NavigationRouteNames.valueOf(routeEnum)) + routeEnum: String + ): String? { + val mapper: ContentSquareScreenMapper by rememberInstance() + return mapper.map(NavigationRouteNames.valueOf(routeEnum)) + } + @Requirement( + "O.Data_6#5", + "O.Purp_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "...is recorded only after the user has given their consent." + ) + @Requirement( + "O.Purp_2#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Tracking of screen names for analytics purposes." + ) + @Requirement( + "A_24525#4", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Tracking of screen names after the user has given their consent" + ) + @NonRestartableComposable + @Composable + @Suppress("ComposableNaming") + fun trackScreen( + screenName: String + ) { LaunchedEffect(screenName) { - if (isAnalyticsAllowed.invoke().first()) { - screenName?.let { block(it) } + withContext(dispatcher) { + try { + if (isAnalyticsAllowedUseCase.invoke().first()) { + contentSquareTracker.track(screenName) + if (BuildConfigExtension.isNonReleaseMode) { + debugTracker.track(screenName) + } + } else { + Napier.d { "Analytics not allowed for screens" } + } + } catch (e: Throwable) { + Napier.e { "error on tracking ${e.stackTraceToString()}" } + } } } } - @NonRestartableComposable - @Composable - fun track( - screenAnalyticsMetric: MutableState, - isTrackingPermitted: MutableState - ) { - LaunchedEffect( - screenAnalyticsMetric, - isTrackingPermitted.value, - screenAnalyticsMetric.value?.isNotEmpty() - ) { - screenAnalyticsMetric.value?.let { - contentSquareTracker.track(it) - if (BuildConfigExtension.isNonReleaseMode) { - demoTracker.track(it) + suspend fun trackEvent(event: TrackedEvent) { + withContext(dispatcher) { + try { + if (isAnalyticsAllowedUseCase.invoke().first()) { + contentSquareTracker.send(event.key to event.value) + if (BuildConfigExtension.isNonReleaseMode) { + debugTracker.send(event.key to event.value) + } + } else { + Napier.d { "Analytics not allowed for events" } } + } catch (e: Throwable) { + Napier.e { "error on tracking ${e.stackTraceToString()}" } } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DebugTrackerScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DebugTrackerScreen.kt new file mode 100644 index 00000000..69895493 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DebugTrackerScreen.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.analytics.presentation.DebugTrackerScreenEvent +import de.gematik.ti.erp.app.analytics.presentation.DebugTrackerScreenEvent.DynamicEvent +import de.gematik.ti.erp.app.analytics.presentation.DebugTrackerScreenEvent.ScreenEvent +import de.gematik.ti.erp.app.analytics.presentation.DebugTrackerScreenViewModel +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BigFontPreview +import de.gematik.ti.erp.app.utils.compose.Center +import de.gematik.ti.erp.app.utils.compose.ErezeptText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigateBackButton +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import io.github.aakira.napier.Napier +import org.kodein.di.compose.rememberInstance + +class DebugTrackerScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + val viewModel by rememberInstance() + val events by viewModel.session.collectAsStateWithLifecycle() + events.forEach { + Napier.d { "DebugTrackerScreen---> $it" } + } + DebugTrackerScaffold( + events = events, + onBack = { navController.popBackStack() } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DebugTrackerScaffold( + events: List, + onBack: () -> Unit +) { + val lazyListState = rememberLazyListState() + AnimatedElevationScaffold( + listState = lazyListState, + topBar = { elevated -> + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors(containerColor = AppTheme.colors.neutral025), + title = { + AnimatedContent( + targetState = elevated, + label = "DebugTrackerScaffold", + transitionSpec = { + if (targetState) { + slideInVertically(initialOffsetY = { -it }) + fadeIn() togetherWith + slideOutVertically(targetOffsetY = { it }) + fadeOut() + } else { + slideInVertically(initialOffsetY = { it }) + fadeIn() togetherWith + slideOutVertically(targetOffsetY = { -it }) + fadeOut() + }.using(SizeTransform(clip = false)) + } + ) { isElevated -> + if (isElevated) { + Center { + Text( + text = stringResource(R.string.tracked_events_title), + color = AppTheme.colors.neutral800, + style = AppTheme.typography.h5 + ) + } + } else { + Text( + text = stringResource(R.string.tracked_events_title), + color = AppTheme.colors.neutral800, + style = AppTheme.typography.h6 + ) + } + } + }, + navigationIcon = { + NavigateBackButton { onBack() } + } + ) + } + ) { + LazyColumn( + modifier = Modifier.padding(it), + state = lazyListState + ) { + itemsIndexed(events) { index, event -> + SpacerMedium() + when (event) { + is DynamicEvent -> { + Center { + Card( + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral025) + ) { + Row( + modifier = Modifier.padding(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Card( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + backgroundColor = AppTheme.colors.primary100, + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral025) + ) { + ErezeptText.Body( + modifier = Modifier + .width(SizeDefaults.twentyfivefold) + .padding(PaddingDefaults.Medium), + text = event.key + ) + } + RightwardArrowLine() + Card( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium), + backgroundColor = AppTheme.colors.primary100, + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral025) + ) { + ErezeptText.Body( + modifier = Modifier.padding(PaddingDefaults.Small), + text = event.value + ) + } + } + } + } + } + + is ScreenEvent -> { + Center { + Card( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium), + backgroundColor = AppTheme.colors.primary100, + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral025) + ) { + Column(modifier = Modifier.padding(PaddingDefaults.Small)) { + ErezeptText.Body(event.value) + } + } + } + } + } + + if (index != events.size - 1) { + Center { + DownArrowLine() + } + } + } + item { SpacerMedium() } + } + } +} + +@Suppress("MagicNumber") +@Composable +private fun DownArrowLine( + height: Dp = SizeDefaults.fivefold, + lineColor: Color = AppTheme.colors.primary200 +) { + Canvas(modifier = Modifier.height(height)) { + // Get the canvas width and height + val canvasWidth = size.width + val canvasHeight = size.height + + // Draw a line starting from the top-center to the bottom-center + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = canvasWidth / 2, y = 0f), + end = Offset(x = canvasWidth / 2, y = canvasHeight), + cap = Stroke.DefaultCap + ) + + // Draw an arrowhead at the bottom-center + val arrowSize = 20f + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = canvasWidth / 2 - arrowSize, y = canvasHeight - 20f), // Left side of arrow + end = Offset(x = canvasWidth / 2, y = canvasHeight), // Tip of the arrow + cap = Stroke.DefaultCap + ) + + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = canvasWidth / 2 + arrowSize, y = canvasHeight - 20f), // Right side of arrow + end = Offset(x = canvasWidth / 2, y = canvasHeight), // Tip of the arrow + cap = Stroke.DefaultCap + ) + } +} + +@Suppress("MagicNumber") +@Composable +fun RightwardArrowLine( + height: Dp = SizeDefaults.fivefold, + lineColor: Color = AppTheme.colors.primary200 +) { + Canvas( + modifier = Modifier.width(height) + ) { + // Get the canvas width and height + val canvasWidth = size.width + val canvasHeight = size.height + + // Draw a horizontal line from the left-center to near the right-end + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = 0f, y = canvasHeight / 2), + end = Offset(x = canvasWidth - 20f, y = canvasHeight / 2), // Leave space for arrowhead + cap = Stroke.DefaultCap + ) + + // Draw the arrowhead at the end of the line (right side) + val arrowSize = 20f + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = canvasWidth - 20f, y = canvasHeight / 2 - arrowSize / 2), // Top part of arrow + end = Offset(x = canvasWidth, y = canvasHeight / 2), // Tip of the arrow + cap = Stroke.DefaultCap + ) + + drawLine( + color = lineColor, + strokeWidth = 5f, + start = Offset(x = canvasWidth - 20f, y = canvasHeight / 2 + arrowSize / 2), // Bottom part of arrow + end = Offset(x = canvasWidth, y = canvasHeight / 2), // Tip of the arrow + cap = Stroke.DefaultCap + ) + } +} + +@LightDarkPreview +@BigFontPreview +@Composable +fun DemoTrackerScreenContentPreview() { + PreviewAppTheme { + DebugTrackerScaffold( + listOf( + ScreenEvent("Screen 1"), + DynamicEvent("Count", "1"), + ScreenEvent("Screen 1"), + DynamicEvent("some long value being sent to be shared here", "3") + ) + ) {} + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DemoTrackerScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DemoTrackerScreen.kt deleted file mode 100644 index 2e70aa8d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/DemoTrackerScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.ui - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.analytics.presentation.DemoTrackerScreenViewModel -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import org.kodein.di.compose.rememberInstance - -class DemoTrackerScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - - @Composable - override fun Content() { - val viewModel by rememberInstance() - val screens by viewModel.session.collectAsStateWithLifecycle() - DemoTrackerScreenContent(screens) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DemoTrackerScreenContent(screens: List) { - Scaffold( - containerColor = AppTheme.colors.neutral025, - contentColor = AppTheme.colors.neutral999, - topBar = { - TopAppBar( - colors = TopAppBarDefaults - .topAppBarColors( - containerColor = AppTheme.colors.neutral025 - ), - title = { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - color = AppTheme.colors.neutral999, - text = "Tracked Screens" - ) - } - ) - } - ) { - LazyColumn( - modifier = Modifier.padding(it) - ) { - items(screens) { screen -> - SpacerMedium() - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - // • = \u2022, - // ● = \u25CF, - // ○ = \u25CB, - // ▪ = \u25AA, - // ■ = \u25A0, - // □ = \u25A1, - // ► = \u25BA - text = "\u25BA $screen" - ) - } - item { SpacerMedium() } - item { Divider() } - } - } -} - -@LightDarkPreview -@Composable -fun DemoTrackerScreenContentPreview() { - PreviewAppTheme { - DemoTrackerScreenContent(listOf("1", "2", "3")) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreen.kt index 4502a36a..7181208c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.ui @@ -37,37 +37,35 @@ import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.annotatedStringBold import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold import de.gematik.ti.erp.app.utils.compose.shortToast @Requirement( - "A_19087", - "A_19088#1", - "A_19091#1", - "A_19092", - "A_19181-01#1", + "A_19091-01#3", + "A_19092-01#3", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Display opt-in for analytics. Full app functionality is available also without opting in." ) class OnboardingAllowAnalyticsScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { @Composable override fun Content() { - val onboardingController = rememberOnboardingController() val context = LocalContext.current val allowStars = stringResource(R.string.settings_tracking_allow_emoji) val allowText = annotatedStringResource( @@ -82,15 +80,20 @@ class OnboardingAllowAnalyticsScreen( navigationMode = NavigationBarMode.Back, topBarTitle = stringResource(R.string.settings_tracking_allow_title), onBack = { - onboardingController.changeAnalyticsState(false) + graphController.changeAnalyticsState(false) context.shortToast(disallowText) navController.popBackStack() }, listState = lazyListState, bottomBar = { + @Requirement( + "O.Data_6#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "...user opts-in for analytics." + ) UserConfirmationBottomBar( onClick = { - onboardingController.changeAnalyticsState(true) + graphController.changeAnalyticsState(true) context.shortToast(allowText) navController.popBackStack() } @@ -106,7 +109,7 @@ class OnboardingAllowAnalyticsScreen( } @Requirement( - "A_19091#2", + "A_19091-01#2", sourceSpecification = "gemSpec_eRp_FdV", rationale = "User confirms the opt in" ) @@ -124,8 +127,7 @@ private fun UserConfirmationBottomBar( } @Requirement( - "A_19089#1", - "A_19090", + "A_19090-01#2", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Display explanation of data processing for analytics opt-in" ) @@ -186,22 +188,21 @@ private fun AllowAnalyticsContent( @LightDarkPreview @Composable -fun AllowAnalyticsContentPreview() { - val state = rememberLazyListState() - PreviewAppTheme { - AllowAnalyticsContent( - lazyListState = state, - paddingValues = PaddingValues(horizontal = PaddingDefaults.Medium) - ) - } -} +fun AllowAnalyticsScreenScaffoldPreview() { + val lazyListState = rememberLazyListState() -@LightDarkPreview -@Composable -fun UserConfirmationBottomBarPreview() { PreviewAppTheme { - UserConfirmationBottomBar( - onClick = {} - ) + TestScaffold( + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(R.string.settings_tracking_allow_title), + bottomBar = { + UserConfirmationBottomBar {} + } + ) { + AllowAnalyticsContent( + lazyListState = lazyListState, + paddingValues = it + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt index e8221321..be773340 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDebugTrackingSessionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDebugTrackingSessionUseCase.kt new file mode 100644 index 00000000..36ac7a40 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDebugTrackingSessionUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.usecase + +import de.gematik.ti.erp.app.analytics.tracker.DebugTrackerSession +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GetDebugTrackingSessionUseCase( + private val session: DebugTrackerSession, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(): List = withContext(dispatcher) { + session.getScreens() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDemoTrackingSessionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDemoTrackingSessionUseCase.kt deleted file mode 100644 index 77ca76d5..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/GetDemoTrackingSessionUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.analytics.usecase - -import de.gematik.ti.erp.app.analytics.tracker.DemoTrackerSession -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class GetDemoTrackingSessionUseCase( - private val session: DemoTrackerSession, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - suspend operator fun invoke(): List = withContext(dispatcher) { - session.getScreens() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StartTrackerUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StartTrackerUseCase.kt new file mode 100644 index 00000000..bd73c66b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StartTrackerUseCase.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.usecase + +import android.content.Context +import com.contentsquare.android.Contentsquare +import de.gematik.ti.erp.app.Requirement +import io.github.aakira.napier.Napier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Requirement( + "A_19096-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "On every app session the old session is forcefully forgotten before we start a new one." +) +class StartTrackerUseCase(val context: Context) { + @Requirement( + "A_19090-01#1", + "A_19089-01#1", + "A_19091-01#1", + "A_19092-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Resets the analytics id on each new session and creates a new one." + ) + suspend operator fun invoke() = withContext(Dispatchers.Main) { + try { + Contentsquare.start(context) + Contentsquare.optOut(context) // reset the analytics id on each new session + Contentsquare.optIn(context) // create a new one + } catch (e: Exception) { + Napier.e { "Error trying to start tracker ${e.stackTraceToString()}" } + // ignore + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StopTrackerUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StopTrackerUseCase.kt new file mode 100644 index 00000000..265b08a7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/StopTrackerUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.usecase + +import android.content.Context +import com.contentsquare.android.Contentsquare +import io.github.aakira.napier.Napier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class StopTrackerUseCase(val context: Context) { + + suspend operator fun invoke() = withContext(Dispatchers.Main) { + try { + Contentsquare.optOut(context) + } catch (e: Exception) { + Napier.e { "Error trying to stop tracker ${e.stackTraceToString()}" } + // ignore + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationInnerPadding.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationInnerPadding.kt new file mode 100644 index 00000000..2dc71722 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationInnerPadding.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.app + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import de.gematik.ti.erp.app.theme.SizeDefaults + +/** + * Since we have a Scaffold for the app and we have multiple scaffolds inside it, + * we use the [ApplicationInnerPadding] to combine them both + */ +data class ApplicationInnerPadding( + val layoutDirection: LayoutDirection, + val applicationScaffoldPadding: PaddingValues = PaddingValues() +) { + fun combineWithInnerScaffold( + innerScaffoldPaddingValues: PaddingValues + ): PaddingValues { + val top = innerScaffoldPaddingValues.calculateTopPadding() + + applicationScaffoldPadding.calculateTopPadding() + + // in scenarios where the padding is not set, we use a default value + val bottom: Dp by lazy { + val calculatedPadding = innerScaffoldPaddingValues.calculateBottomPadding() + applicationScaffoldPadding.calculateBottomPadding() + if (calculatedPadding == SizeDefaults.zero) { + SizeDefaults.tenfold // 80.dp + } else { + calculatedPadding + } + } + + val start = innerScaffoldPaddingValues.calculateStartPadding(layoutDirection) + + applicationScaffoldPadding.calculateStartPadding(layoutDirection) + + val end = innerScaffoldPaddingValues.calculateEndPadding(layoutDirection) + + applicationScaffoldPadding.calculateEndPadding(layoutDirection) + + return PaddingValues( + start = start, + top = top, + end = end, + bottom = bottom + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationScaffold.kt new file mode 100644 index 00000000..fae2b7f7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/app/ApplicationScaffold.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.app + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.compose.currentBackStackEntryAsState +import de.gematik.ti.erp.app.base.model.DownloadResourcesState.Companion.isInProgress +import de.gematik.ti.erp.app.core.LocalNavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.NavigationGraph +import de.gematik.ti.erp.app.mainscreen.presentation.rememberAppController +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenBottomBar +import de.gematik.ti.erp.app.mainscreen.ui.OrderStateChangeOnSuccessSideEffect +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutes +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.SimpleBanner +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold + +@Composable +fun ApplicationScaffold( + authentication: AuthenticationModeAndMethod?, + isDemoMode: Boolean +) { + val navController = LocalNavController.current + val context = LocalContext.current + + val scope = rememberCoroutineScope() + val appController = rememberAppController() + + val refreshState by appController.refreshState.collectAsStateWithLifecycle() + + val currentRoute by navController.currentBackStackEntryAsState() + val activeProfile by appController.activeProfile.collectAsStateWithLifecycle() + val orderEventState by appController.orderedEvent.collectAsStateWithLifecycle() + val isNetworkConnected by appController.isNetworkConnected.collectAsStateWithLifecycle() + + val snackbarHostState = remember { SnackbarHostState() } + val unreadOrdersCount by appController.unreadOrders.collectAsStateWithLifecycle() + + LaunchedEffect(orderEventState) { + OrderStateChangeOnSuccessSideEffect( + context = context, + scope = scope, + snackbar = snackbarHostState, + orderedEvent = orderEventState, + resetOrdered = appController::resetOrderedEvent + ) + } + + LaunchedEffect(refreshState) { + if (!refreshState.isInProgress()) { + activeProfile.data?.let { + appController.updateUnreadOrders(it) + } + } + } + + val bottomRoutes = listOf( + PrescriptionRoutes.PrescriptionsScreen.route, + PharmacyRoutes.PharmacyStartScreen.route, + MessagesRoutes.MessageListScreen.route, + SettingsNavigationScreens.SettingsScreen.route + ) + + val isBottomSheetScreen = remember(currentRoute) { + currentRoute?.let { + // todo: use MainScreenBottomNavigationItems instead of bottomRoutes + bottomRoutes.contains(it.destination.route) + } ?: false + } + + LaunchedEffect(currentRoute) { + activeProfile.data?.let { + appController.updateUnreadOrders(it) + } + } + + CompositionLocalProvider( + LocalSnackbarScaffold provides snackbarHostState + ) { + val snackbar = LocalSnackbarScaffold.current + val layoutDirection = LocalLayoutDirection.current + Scaffold( + snackbarHost = { SnackbarHost(snackbar, modifier = Modifier.systemBarsPadding()) }, + content = { innerPadding -> + Column { + AnimatedVisibility( + modifier = Modifier.align(Alignment.CenterHorizontally), + visible = !isNetworkConnected, + enter = fadeIn() + expandVertically(expandFrom = Alignment.Top) + slideInVertically(), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top) + slideOutVertically() + ) { + SimpleBanner( + modifier = Modifier.padding(top = PaddingDefaults.Tiny), + containerColor = AppTheme.colors.neutral200, + text = stringResource(R.string.no_internet_text) + ) + } + NavigationGraph( + authentication = authentication, + isDemoMode = isDemoMode, + // needed for fab for screens which have scaffolds + padding = ApplicationInnerPadding(layoutDirection, innerPadding) + ) + } + }, + bottomBar = { + if (isBottomSheetScreen) { + MainScreenBottomBar( + mainNavController = navController, + unreadOrdersCount = unreadOrdersCount + ) + } + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt index 25020e4d..07da9cf6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt index 96711ac9..0625532c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt index d6214b5a..fc7176d7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.navigation @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.appsecurity.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes.DeviceCheckLoadingScreen import de.gematik.ti.erp.app.appsecurity.ui.DeviceCheckLoadingStartScreen import de.gematik.ti.erp.app.appsecurity.ui.InsecureDeviceScreen @@ -51,6 +52,11 @@ fun NavGraphBuilder.appSecurityGraph( when { appSecurityResult.isIntegritySecure && appSecurityResult.isDeviceSecure -> onAppSecurityPassed() appSecurityResult.isIntegritySecure && !appSecurityResult.isDeviceSecure -> + @Requirement( + "O.Plat_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for insecure Devices on app start and during onboarding." + ) navController.navigate(AppSecurityRoutes.InsecureDeviceScreen.path()) { launchSingleTop = true } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt index 02c20e6a..9085d4af 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.navigation @@ -28,16 +28,16 @@ object AppSecurityRoutes : NavigationRoutes { override fun subGraphName() = "app_security" - const val IntegrityWarningScreenArgument = "isDeviceRiskAccepted" + const val APP_SECURITY_NAV_IS_RISK_ACCEPTED = "isDeviceRiskAccepted" object DeviceCheckLoadingScreen : Routes(NavigationRouteNames.DeviceCheckLoadingScreen.name) object InsecureDeviceScreen : Routes(NavigationRouteNames.InsecureDeviceScreen.name) object IntegrityWarningScreen : Routes( path = NavigationRouteNames.IntegrityWarningScreen.name, - navArgument(IntegrityWarningScreenArgument) { type = NavType.StringType } + navArgument(APP_SECURITY_NAV_IS_RISK_ACCEPTED) { type = NavType.StringType } ) { fun path(appSecurityResult: String) = - path(IntegrityWarningScreenArgument to appSecurityResult) + path(APP_SECURITY_NAV_IS_RISK_ACCEPTED to appSecurityResult) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt index deef855e..491ec873 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.presentation @@ -24,6 +24,7 @@ import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.appsecurity.usecase.GetDeviceSecurityUseCase import de.gematik.ti.erp.app.appsecurity.usecase.IntegrityUseCase import de.gematik.ti.erp.app.appsecurity.usecase.IsIntegrityRiskAcceptedUseCase +import de.gematik.ti.erp.app.base.Controller import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -33,7 +34,8 @@ class AppSecurityController( private val integrityUseCase: IntegrityUseCase, private val isIntegrityRiskAcceptedUseCase: IsIntegrityRiskAcceptedUseCase, private val deviceSecurityUseCase: GetDeviceSecurityUseCase -) { +) : Controller() { + @Requirement( "O.Arch_6#2", "O.Resi_2#2", @@ -41,7 +43,8 @@ class AppSecurityController( "O.Resi_4#2", "O.Resi_5#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "Check device integrity." + rationale = "Combine integrity check and risk acceptance to determine if the app is secure " + + "or the risk was accepted." ) suspend fun checkIntegrityRisk() = combine( @@ -63,9 +66,9 @@ class AppSecurityController( }.first() @Requirement( - "O.Plat_1#2", + "O.Plat_1#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for insecure Devices on first screen when the app is started." + rationale = "Insecure Devices warning screen that is shown to the user to make a informed decision." ) suspend fun checkDeviceSecurityRisk(): Boolean = deviceSecurityUseCase.invoke().first() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt index c5bffe24..1d371117 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt index 507b0d12..cb91c91e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt index bdd41439..7d83a1f0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.ui @@ -75,16 +75,27 @@ class DeviceCheckLoadingStartScreen( ) LaunchedEffect(Unit) { - val isIntegritySecure = appSecurityController.checkIntegrityRisk() - val isDeviceSecure = appSecurityController.checkDeviceSecurityRisk() - // on obtaining results make a callback to use the results - scope.launch { - onSecurityCheckResult( - AppSecurityResult( - isIntegritySecure = isIntegritySecure, - isDeviceSecure = isDeviceSecure + if (BuildConfigExtension.isInternalDebug) { + scope.launch { + onSecurityCheckResult( + AppSecurityResult( + isIntegritySecure = true, + isDeviceSecure = true + ) ) - ) + } + } else { + val isIntegritySecure = appSecurityController.checkIntegrityRisk() + val isDeviceSecure = appSecurityController.checkDeviceSecurityRisk() + // on obtaining results make a callback to use the results + scope.launch { + onSecurityCheckResult( + AppSecurityResult( + isIntegritySecure = isIntegritySecure, + isDeviceSecure = isDeviceSecure + ) + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt index 8b6182e1..d5377672 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt @@ -1,32 +1,33 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.ui import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -39,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement @@ -50,38 +52,38 @@ import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingControlle import de.gematik.ti.erp.app.onboarding.ui.SkipOnBoardingButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.Toggle +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension import java.util.Locale @Requirement( "O.Plat_1#4", + "O.Resi_1#5", sourceSpecification = "BSI-eRp-ePA", - rationale = "insecure Devices warning." + rationale = "Insecure Devices warning screen that is shown to the user to make a informed decision." ) class InsecureDeviceScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry, val onBack: () -> Unit ) : Screen() { - @Composable override fun Content() { var checked by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberScrollState() - + val listState = rememberLazyListState() val insecureDeviceController = rememberInsecureDeviceController() - // used here only for skip button val onboardingController = rememberOnboardingController() - AnimatedElevationScaffold( - elevated = scrollState.value > 0, + listState = listState, navigationMode = NavigationBarMode.Close, bottomBar = { BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { @@ -106,36 +108,7 @@ class InsecureDeviceScreen( topBarTitle = stringResource(id = R.string.insecure_device_title), onBack = onBack ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(scrollState) - .padding(PaddingDefaults.Medium) - ) { - Image( - painterResource(id = R.drawable.laptop_woman_yellow), - null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxSize() - ) - SpacerSmall() - Text( - stringResource(id = R.string.insecure_device_header), - style = AppTheme.typography.h6 - ) - SpacerSmall() - Text( - stringResource(id = R.string.insecure_device_info), - style = AppTheme.typography.body1 - ) - Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) - Toggle( - checked = checked, - onCheckedChange = { checked = it }, - description = stringResource(id = R.string.insecure_device_accept) - ) - } + InsecureDeviceScreenContent(listState, innerPadding, { checked = it }, checked) } if (BuildConfigExtension.isNonReleaseMode) { @@ -146,3 +119,59 @@ class InsecureDeviceScreen( } } } + +@Composable +private fun InsecureDeviceScreenContent( + listState: LazyListState, + innerPadding: PaddingValues, + onToggle: (Boolean) -> Unit, + checked: Boolean +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(PaddingDefaults.Medium) + ) { + item { + Image( + painterResource(id = R.drawable.laptop_woman_yellow), + null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxSize() + ) + SpacerSmall() + } + item { + Text( + stringResource(id = R.string.insecure_device_header), + style = AppTheme.typography.h6 + ) + SpacerSmall() + } + item { + Text( + stringResource(id = R.string.insecure_device_info), + style = AppTheme.typography.body1 + ) + Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) + } + item { + Toggle( + checked = checked, + onCheckedChange = { onToggle(it) }, + description = stringResource(id = R.string.insecure_device_accept) + ) + } + } +} + +@LightDarkPreview +@Composable +fun InsecureDeviceScreenPreview(@PreviewParameter(BooleanPreviewParameterProvider::class) checked: Boolean) { + val lazyListState = rememberLazyListState() + PreviewAppTheme { + InsecureDeviceScreenContent(lazyListState, PaddingValues(PaddingDefaults.Medium), { }, checked) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt index 3a7c1679..9ae33783 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt @@ -1,32 +1,33 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.ui import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -40,6 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement @@ -54,54 +56,53 @@ import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingControlle import de.gematik.ti.erp.app.onboarding.ui.SkipOnBoardingButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.Toggle +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension import io.github.aakira.napier.Napier import java.util.Locale -@Requirement( - "O.Arch_6#3", - "O.Resi_2#3", - "O.Resi_3#3", - "O.Resi_4#3", - "O.Resi_5#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Show integrity warning." -) -@Requirement( - "A_21574", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Show integrity warning." -) class IntegrityWarningScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry, val onBack: (Boolean) -> Unit ) : Screen() { + + @Requirement( + "O.Arch_6#1", + "O.Resi_2#3", + "O.Resi_3#3", + "O.Resi_4#3", + "O.Resi_5#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Show integrity warning to the user on a rooted device." + ) @Composable override fun Content() { val result = remember { - navBackStackEntry.arguments?.getString(AppSecurityRoutes.IntegrityWarningScreenArgument) ?: "" + navBackStackEntry.arguments?.getString(AppSecurityRoutes.APP_SECURITY_NAV_IS_RISK_ACCEPTED) ?: "" } val appSecurityResult = fromNavigationString(result) Napier.d { "app security result $appSecurityResult" } var checked by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberScrollState() + val listState = rememberLazyListState() val integrityWarningController = rememberIntegrityWarningController() // used here only for skip button val onboardingController = rememberOnboardingController() AnimatedElevationScaffold( - elevated = scrollState.value > 0, + listState = listState, navigationMode = NavigationBarMode.Close, bottomBar = { BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { @@ -128,63 +129,89 @@ class IntegrityWarningScreen( onBack(appSecurityResult.isDeviceSecure) } ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(scrollState) - .padding(PaddingDefaults.Medium) - ) { - Image( - painterResource(id = R.drawable.laptop_woman_pink), - null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxSize() - ) - SpacerSmall() - Text( - stringResource(id = R.string.insecure_device_header_safetynet), - style = AppTheme.typography.h6 - ) - SpacerSmall() - Text( - stringResource(id = R.string.insecure_device_info_safetynet), - style = AppTheme.typography.body1 - ) - // Todo wait for new uri from security staff -// val uriHandler = LocalUriHandler.current -// SpacerMedium() -// Text( -// stringResource(R.string.insecure_device_safetynet_more_info), -// style = AppTheme.typography.body2, -// color = AppTheme.colors.neutral600 -// ) -// SpacerSmall() -// val link = stringResource(R.string.insecure_device_safetynet_link) -// TextButton( -// modifier = Modifier.align(Alignment.End), -// onClick = { uriHandler.openUri(link) } -// ) { -// Text( -// stringResource(id = R.string.insecure_device_safetynet_link_text), -// style = AppTheme.typography.body2, -// color = AppTheme.colors.primary600 -// ) -// } - Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) - Toggle( - checked = checked, - onCheckedChange = { checked = it }, - description = stringResource(id = R.string.insecure_device_accept_safetynet) - ) - - if (BuildConfigExtension.isNonReleaseMode) { - SkipOnBoardingButton { - onboardingController.createProfileOnSkipOnboarding() - navController.finishOnboardingAsSuccessAndOpenPrescriptions() - } - } + IntegrityWarningScreenContent(listState, innerPadding, { checked = it }, checked) + } + if (BuildConfigExtension.isNonReleaseMode) { + SkipOnBoardingButton { + onboardingController.createProfileOnSkipOnboarding() + navController.finishOnboardingAsSuccessAndOpenPrescriptions() } } } } + +@Composable +private fun IntegrityWarningScreenContent( + listState: LazyListState, + innerPadding: PaddingValues, + onToggle: (Boolean) -> Unit, + checked: Boolean +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(PaddingDefaults.Medium) + ) { + item { + Image( + painterResource(id = R.drawable.laptop_woman_pink), + null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxSize() + ) + } + item { + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_header_safetynet), + style = AppTheme.typography.h6 + ) + } + item { + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_info_safetynet), + style = AppTheme.typography.body1 + ) + } + // Todo wait for new uri from security staff + // val uriHandler = LocalUriHandler.current + // SpacerMedium() + // Text( + // stringResource(R.string.insecure_device_safetynet_more_info), + // style = AppTheme.typography.body2, + // color = AppTheme.colors.neutral600 + // ) + // SpacerSmall() + // val link = stringResource(R.string.insecure_device_safetynet_link) + // TextButton( + // modifier = Modifier.align(Alignment.End), + // onClick = { uriHandler.openUri(link) } + // ) { + // Text( + // stringResource(id = R.string.insecure_device_safetynet_link_text), + // style = AppTheme.typography.body2, + // color = AppTheme.colors.primary600 + // ) + // } + item { + Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) + Toggle( + checked = checked, + onCheckedChange = { onToggle(it) }, + description = stringResource(id = R.string.insecure_device_accept_safetynet) + ) + } + } +} + +@LightDarkPreview +@Composable +fun IntegrityWarningScreenPreview(@PreviewParameter(BooleanPreviewParameterProvider::class) checked: Boolean) { + val lazyListState = rememberLazyListState() + PreviewAppTheme { + IntegrityWarningScreenContent(lazyListState, PaddingValues(PaddingDefaults.Medium), { }, checked) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt index 7f18c645..9e652425 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.ui.model diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt index f908d7f7..0afd1b28 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt index 7746ee56..3b58c245 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt index ac9af4bf..7efafd54 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt index 1bcbde95..b54a90e0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase @@ -37,9 +37,10 @@ class GetDeviceSecurityUseCase( private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { @Requirement( - "O.Plat_1#1", + "O.Plat_1#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for insecure Devices." + rationale = "System call to check for insecure Devices. If the check fails the user is informed that " + + "they are at their own risk if they are using the app." ) operator fun invoke(): Flow = settingsRepository.general diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt index 2fc86395..bb688391 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase @@ -52,17 +52,13 @@ class IntegrityUseCase( private val context: Context ) { @Requirement( - "O.Arch_6#1", + "O.Arch_6#3", "O.Resi_2#1", "O.Resi_3#1", "O.Resi_4#1", "O.Resi_5#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "In the release process, the app is signed and the signed app bundle is uploaded to the store. " + - "An altered application can only run on a jail-broken device. We are using Googles Integrity API " + - "to detect jail-broken devices. If a user is using a jail-broken device, may it be known or unknown, " + - "we display a security alert so a user can make an informed decision to use or " + - "not use the application.." + rationale = "Run integrity check against the IntegrityManagerFactory provided by Google Play Core Library." ) fun runIntegrityAttestation(): Flow = flow { val salt = provideSalt() @@ -81,9 +77,9 @@ class IntegrityUseCase( @Requirement( "O.Cryp_1#6", - "O.Cryp_4#6", + "O.Cryp_4#8", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature via ecdh ephemeral-static [one time usage]" ) val decryptionKeyBytes: ByteArray = Base64.decode(BuildKonfig.INTEGRITY_API_KEY, Base64.DEFAULT) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt index 3c217393..e9e59d52 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appsecurity.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/datasource/local/AppUpdateManagerSelectionLocalDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/datasource/local/AppUpdateManagerSelectionLocalDataSource.kt index 93c465dd..f72908ca 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/datasource/local/AppUpdateManagerSelectionLocalDataSource.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/datasource/local/AppUpdateManagerSelectionLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.datasource.local diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/di/AppUpdateModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/di/AppUpdateModule.kt index f33cffc6..ee6de84f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/di/AppUpdateModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/di/AppUpdateModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.di @@ -22,6 +22,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.appupdate.datasource.local.AppUpdateManagerSelectionLocalDataSource import de.gematik.ti.erp.app.appupdate.repository.AppUpdateFlagRepository import de.gematik.ti.erp.app.appupdate.repository.AppUpdateManagerSelectionRepository @@ -54,6 +55,12 @@ val appUpdateModule = DI.Module("appUpdateModule") { val context = instance() val masterKey = instance(ENCRYPTED_PREFS_MASTER_KEY_ALIAS) + @Requirement( + "O.Data_2#2", + "O.Data_3#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Data storage using EncryptedSharedPreferences." + ) EncryptedSharedPreferences.create( context, ENCRYPTED_PREFS_FILE_NAME, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateGraph.kt index be520b2c..60c136e8 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateRoutes.kt index 3879f1cf..ab6508bb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/navigation/AppUpdateRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateFlagRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateFlagRepository.kt index d19b1499..727f8045 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateFlagRepository.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateFlagRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.repository diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateManagerSelectionRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateManagerSelectionRepository.kt index 06de256c..c9f2fee7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateManagerSelectionRepository.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/repository/AppUpdateManagerSelectionRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.repository diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/ui/AppUpdateScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/ui/AppUpdateScreen.kt index 8da59d0b..2a7f5966 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/ui/AppUpdateScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/ui/AppUpdateScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.ui @@ -43,14 +43,16 @@ import androidx.compose.ui.text.style.TextAlign import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.appupdate.usecase.ChangeAppUpdateFlagUseCase +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.extensions.openAppPlayStoreLink import org.kodein.di.compose.rememberInstance @@ -61,15 +63,19 @@ class AppUpdateScreen( @Composable override fun Content() { - UpdateAppScreenContent() + val useCase by rememberInstance() + UpdateAppScreenContent { useCase.invoke(false) } } } @Composable -private fun UpdateAppScreenContent() { +private fun UpdateAppScreenContent( + onClick: () -> Unit +) { val context = LocalContext.current - val useCase by rememberInstance() + val padding = (LocalActivity.current as? BaseActivity)?.applicationInnerPadding + // TODO: Convert to AnimatedElevationScaffold Scaffold( topBar = { Row( @@ -97,7 +103,7 @@ private fun UpdateAppScreenContent() { Column( modifier = Modifier .fillMaxSize() - .padding(innerPadding) + .padding(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) .padding(PaddingDefaults.Large), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center @@ -148,9 +154,7 @@ private fun UpdateAppScreenContent() { ) } TextButton( - onClick = { - useCase.invoke(false) - } + onClick = onClick ) { Text( stringResource(R.string.cancel) @@ -166,6 +170,6 @@ private fun UpdateAppScreenContent() { @Composable fun UpdateAppScreenContentPreview() { PreviewAppTheme { - UpdateAppScreenContent() + UpdateAppScreenContent {} } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/AppUpdateInfoUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/AppUpdateInfoUseCase.kt index dd61ab7b..d3d44659 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/AppUpdateInfoUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/AppUpdateInfoUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateFlagUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateFlagUseCase.kt index 9dcf92f5..43becfd7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateFlagUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateFlagUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateManagerFlagUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateManagerFlagUseCase.kt index 1607cc20..605cf88e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateManagerFlagUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/ChangeAppUpdateManagerFlagUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateFlagUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateFlagUseCase.kt index 5e4603a9..0d5ac842 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateFlagUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateFlagUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerFlagUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerFlagUseCase.kt index 98496bf6..19907604 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerFlagUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerFlagUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerUseCase.kt index cd5e788a..b3855c51 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appupdate/usecase/GetAppUpdateManagerUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt index e4bd34c3..74915968 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.authentication.di diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/AuthenticationFailureDialogMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/AuthenticationFailureDialogMapper.kt new file mode 100644 index 00000000..4ec61963 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/AuthenticationFailureDialogMapper.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.mapper + +import de.gematik.ti.erp.app.authentication.model.AuthenticationDialogParameter +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.features.R + +fun (AuthenticationResult.Error).toDialogMapper(): AuthenticationDialogParameter? = + when (this) { + AuthenticationResult.IdpCommunicationError.AuthenticationNotSuccessful -> AuthenticationDialogParameter( + title = R.string.cdw_mini_alt_auth_removed_title, + message = R.string.cdw_mini_alt_auth_removed + ) + AuthenticationResult.IdpCommunicationError.CommunicationFailure -> AuthenticationDialogParameter( + title = R.string.cdw_nfc_intro_step1_header_on_error, + message = R.string.cdw_idp_error_time_and_connection + ) + AuthenticationResult.IdpCommunicationError.InvalidCertificate -> AuthenticationDialogParameter( + title = R.string.cdw_nfc_error_title_invalid_certificate, + message = R.string.cdw_nfc_error_body_invalid_certificate + ) + AuthenticationResult.IdpCommunicationError.InvalidOCSP -> AuthenticationDialogParameter( + title = R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate, + message = R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate + ) + // TODO: Add dialog params for SecureElementFailure & UserNotAuthenticated + AuthenticationResult.IdpCommunicationError.SecureElementFailure -> null + AuthenticationResult.IdpCommunicationError.UserNotAuthenticated -> null + is AuthenticationResult.BiometricResult.BiometricError -> null + is AuthenticationResult.IdpCommunicationError.IllegalStateError -> null + is AuthenticationResult.UnknownError -> null + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt index 4740db77..271649b5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt @@ -1,32 +1,32 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.authentication.mapper +import de.gematik.ti.erp.app.authentication.model.Biometric import de.gematik.ti.erp.app.authentication.model.External import de.gematik.ti.erp.app.authentication.model.HealthCard import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData import de.gematik.ti.erp.app.authentication.model.None import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator -import de.gematik.ti.erp.app.authentication.model.SecureElement +import de.gematik.ti.erp.app.cardwall.mini.ui.BiometricPromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalPromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPromptAuthenticator -import de.gematik.ti.erp.app.cardwall.mini.ui.SecureHardwarePromptAuthenticator import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -41,8 +41,8 @@ class DefaultPromptAuthenticationProvider : PromptAuthenticationProvider { val healthCardPrompt = authenticators .filterIsInstance().first() - val securedHardwarePrompt = authenticators - .filterIsInstance().first() + val biometricPrompt = authenticators + .filterIsInstance().first() val externalPrompt = authenticators .filterIsInstance().first() @@ -50,7 +50,7 @@ class DefaultPromptAuthenticationProvider : PromptAuthenticationProvider { return when (initialAuthenticationData) { is External -> externalPrompt.authenticate(id, scope) is HealthCard -> healthCardPrompt.authenticate(id, scope) - is SecureElement -> securedHardwarePrompt.authenticate(id, scope) + is Biometric -> biometricPrompt.authenticate(id, scope) is None -> flowOf(PromptAuthenticator.AuthResult.NoneEnrolled) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationDialogParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationDialogParameter.kt new file mode 100644 index 00000000..59e8373e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationDialogParameter.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.model + +import androidx.annotation.StringRes + +data class AuthenticationDialogParameter( + @StringRes val title: Int, + @StringRes val message: Int +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationResult.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationResult.kt new file mode 100644 index 00000000..8e34d5c1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/AuthenticationResult.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.model + +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState + +sealed interface AuthenticationResult { + + sealed interface Error : AuthenticationResult { + sealed interface ResetError : Error // this error means the user has to re-authenticate using the complete card-wall + sealed interface RetryError : Error // this error means the user has to retry the current step + sealed interface IgnoreError : Error + } + + data object UnknownError : AuthenticationResult, Error.IgnoreError + + // results from the biometric prompt + sealed interface BiometricResult : AuthenticationResult { + data object BiometricStarted : BiometricResult + data object BiometricSuccess : BiometricResult + data class BiometricError(val error: String, val errorCode: Int) : BiometricResult, Error.IgnoreError + } + + // results from the idp communication + sealed interface IdpCommunicationUpdate : AuthenticationResult { + data object IdpCommunicationStarted : IdpCommunicationUpdate + data object IdpCommunicationUpdated : IdpCommunicationUpdate + data object IdpCommunicationSuccess : IdpCommunicationUpdate + } + + // errors from the idp communication + sealed interface IdpCommunicationError : AuthenticationResult { + data object AuthenticationNotSuccessful : IdpCommunicationError, Error.ResetError + data object CommunicationFailure : IdpCommunicationError, Error.RetryError + data object InvalidCertificate : IdpCommunicationError, Error.RetryError + data object InvalidOCSP : IdpCommunicationError, Error.RetryError + data object SecureElementFailure : IdpCommunicationError, Error.ResetError + data object UserNotAuthenticated : IdpCommunicationError, Error.ResetError + data class IllegalStateError(val state: AuthenticationState) : IdpCommunicationError, Error.ResetError + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/CardWallEventData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/CardWallEventData.kt new file mode 100644 index 00000000..8f415b50 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/CardWallEventData.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.model + +/* + * ${GEMATIK_COPYRIGHT_STATEMENT} + */ +data class CardWallEventData( + val profileId: String, + val can: String +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/ChooseAuthenticationController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/ChooseAuthenticationController.kt new file mode 100644 index 00000000..f38c2292 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/ChooseAuthenticationController.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationSuccess +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.presentation.GetProfileByIdController +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +private const val TAG = "AuthenticationController" + +/** + * Include this controller with your viewmodel if you want the screen using the viewmodel to be able to allow the user to login. + * * [showCardWallEvent], the screen should navigate to the cardwall screen. + * * [biometricAuthenticationSuccessEvent], the user has logged in using biometric authentication. + * * [biometricAuthenticationResetErrorEvent], the user has tried to login using biometric authentication but the authentication failed. + * Now the user has to re-authenticate and biometrics won't be called this time + * * [biometricAuthenticationOtherErrorEvent], the user has tried to login using biometric authentication but the authentication failed. + * Now the user has to re-authenticate and biometrics will be called again + */ +abstract class ChooseAuthenticationController( + profileId: ProfileIdentifier? = null, + getProfileByIdUseCase: GetProfileByIdUseCase, + getProfilesUseCase: GetProfilesUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + private val chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + private val biometricAuthenticator: BiometricAuthenticator, + override val onSelectedProfileSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + override val onSelectedProfileFailure: ((Throwable, CoroutineScope) -> Unit)? = null, + override val onActiveProfileSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + override val onActiveProfileFailure: ((Throwable, CoroutineScope) -> Unit)? = null +) : GetProfileByIdController( + selectedProfileId = profileId, + getProfilesUseCase = getProfilesUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + onSelectedProfileSuccess = onSelectedProfileSuccess, + onSelectedProfileFailure = onSelectedProfileFailure, + onActiveProfileSuccess = onActiveProfileSuccess, + onActiveProfileFailure = onActiveProfileFailure +) { + val showCardWallEvent = ComposableEvent() + val showCardWallWithFilledCanEvent = ComposableEvent() + val showGidEvent = ComposableEvent() + protected val biometricAuthenticationSuccessEvent = ComposableEvent() + protected val biometricAuthenticationResetErrorEvent = ComposableEvent() + protected val biometricAuthenticationOtherErrorEvent = ComposableEvent() + + @OptIn(ExperimentalCoroutinesApi::class) + protected fun Flow.onBiometricAuthentication(): Flow = + flatMapLatest { profile -> + chooseAuthenticationDataUseCase(profile.id) + .map { authenticationData -> + when (authenticationData) { + is Biometric -> profile + else -> null + } + } + } + + fun chooseAuthenticationMethod( + profileId: ProfileIdentifier, + useBiometricPairingScope: Boolean = false + ) { + controllerScope.launch { + chooseAuthenticationDataUseCase(profileId).first { authenticationData: InitialAuthenticationData -> + when (authenticationData) { + is Biometric -> { + Napier.i(tag = TAG, message = "trigger biometric authentication") + biometricAuthenticator.authenticate( + id = profileId, + scope = if (useBiometricPairingScope) IdpScope.BiometricPairing else IdpScope.Default + ).collectLatest { result -> + when (result) { + is IdpCommunicationSuccess -> { + refreshCombinedProfile() + refreshActiveProfile() + biometricAuthenticationSuccessEvent.trigger() + } + + is AuthenticationResult.BiometricResult.BiometricError -> { + onRefreshProfileAction.trigger(false) + } + + is AuthenticationResult.Error -> handleAuthenticationError( + profileId = profileId, + error = result + ) + + is AuthenticationResult.BiometricResult.BiometricStarted, + is AuthenticationResult.BiometricResult.BiometricSuccess -> { + onRefreshProfileAction.trigger(true) + } + + is AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationStarted, + AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationUpdated -> { + // do nothing right now + } + } + } + } + + is External -> showGidEvent.trigger(GidEventData(profileId, authenticationData.authenticatorId, authenticationData.authenticatorName)) + is HealthCard -> showCardWallWithFilledCanEvent.trigger(CardWallEventData(profileId, authenticationData.can)) + is None -> showCardWallEvent.trigger(profileId) + } + true + } + } + } + + private fun handleAuthenticationError( + error: AuthenticationResult.Error, + profileId: ProfileIdentifier + ) { + when (error) { + is AuthenticationResult.Error.ResetError -> { + Napier.i(tag = TAG) { "Removing authentication data from database" } + biometricAuthenticator.removeAuthentication(profileId) + refreshCombinedProfile() + refreshActiveProfile() + biometricAuthenticationResetErrorEvent.trigger(error) + } + + else -> { + biometricAuthenticationOtherErrorEvent.trigger(error) + } + } + } +} + +@Composable +fun rememberChooseAuthenticationController(): ChooseAuthenticationController { + val getProfileByIdUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val chooseAuthenticationDataUseCase by rememberInstance() + val biometricAuthenticator = LocalBiometricAuthenticator.current + + return remember { + object : ChooseAuthenticationController( + getActiveProfileUseCase = getActiveProfileUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator + ) {} + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/GidEventData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/GidEventData.kt new file mode 100644 index 00000000..b4e860d4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/model/GidEventData.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.model + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.serialization.Serializable + +/* + * ${GEMATIK_COPYRIGHT_STATEMENT} + */ +@Serializable +data class GidEventData( + val profileId: ProfileIdentifier, + val authenticatorId: String, + val authenticatorName: String +) { + companion object { + fun GidEventData.isPkv() = authenticatorId.endsWith("pkv") + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Authenticator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Authenticator.kt new file mode 100644 index 00000000..93e61c75 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Authenticator.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.presentation + +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow + +abstract class Authenticator : Controller() { + + abstract fun authenticate( + id: ProfileIdentifier, + scope: IdpScope = IdpScope.Default + ): Flow + + abstract fun removeAuthentication(id: ProfileIdentifier) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Biometric.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Biometric.kt new file mode 100644 index 00000000..b670d3f4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/Biometric.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.presentation + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyInfo +import android.security.keystore.KeyProperties +import androidx.biometric.BiometricManager +import org.bouncycastle.util.encoders.Base64 +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.spec.ECGenParameterSpec + +fun deviceSupportsAuthenticationMethod(status: Int) = when (status) { + BiometricManager.BIOMETRIC_SUCCESS, + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> + true + else -> + false +} + +fun deviceHasAuthenticationMethodEnabled(status: Int) = status == BiometricManager.BIOMETRIC_SUCCESS + +fun Context.deviceDeviceSecurityStatus(): Int { + val biometricManager = BiometricManager.from(this) + return biometricManager.canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) +} + +fun Context.deviceBiometricStatus(): Int { + val biometricManager = BiometricManager.from(this) + return biometricManager.canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.BIOMETRIC_WEAK + ) +} + +fun Context.deviceStrongBoxStatus() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + this.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) + } else { + false + } + +fun Context.deviceHardwareBackedKeystoreStatus(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + this.packageManager.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE) || + this.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) + } else { + try { + // create a temporary key pair to check if it is hardware backed + val aliasOfSecureElementEntry = Base64.toBase64String("HardWareBackedAlias".toByteArray()) + + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + "AndroidKeyStore" + ) + val parameterSpec = KeyGenParameterSpec.Builder( + aliasOfSecureElementEntry, + KeyProperties.PURPOSE_SIGN + ).apply { + setDigests(KeyProperties.DIGEST_SHA256) + setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + }.build() + keyPairGenerator.initialize(parameterSpec) + val keyPair = keyPairGenerator.generateKeyPair() + val key = keyPair.private + + val factory = KeyFactory.getInstance(key.algorithm, "AndroidKeyStore") + val keyInfo: KeyInfo = factory.getKeySpec(key, KeyInfo::class.java) + val isHardWareBacked = keyInfo.isInsideSecureHardware + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.deleteEntry(aliasOfSecureElementEntry) + + isHardWareBacked + } catch (e: Exception) { + false + } + } +} +fun enrollBiometricsIntent(): Intent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL) + intent.putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + } else { + Intent(Settings.ACTION_SETTINGS) + } +} + +fun enrollDeviceSecurityIntent(): Intent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Intent(Settings.ACTION_SECURITY_SETTINGS) + } else { + Intent(Settings.ACTION_SETTINGS) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/BiometricAuthenticator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/BiometricAuthenticator.kt new file mode 100644 index 00000000..e7b9f0d7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/presentation/BiometricAuthenticator.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.presentation + +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.BiometricResult +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.BiometricResult.BiometricSuccess +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.AuthenticationNotSuccessful +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.CommunicationFailure +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.IllegalStateError +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.InvalidCertificate +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.InvalidOCSP +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.SecureElementFailure +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationError.UserNotAuthenticated +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationStarted +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationSuccess +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.IdpCommunicationUpdate.IdpCommunicationUpdated +import de.gematik.ti.erp.app.authentication.ui.components.biometricPromptLauncher +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.idp.usecase.RemoveAuthenticationUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class BiometricAuthenticator( + private val authenticationUseCase: AuthenticationUseCase, + private val removeAuthenticationUseCase: RemoveAuthenticationUseCase, + private val promptInfo: BiometricPrompt.PromptInfo, + private val biometricPromptBuilder: BiometricPromptBuilder +) : Authenticator() { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun authenticate( + id: ProfileIdentifier, + scope: IdpScope + ): Flow { + return biometricPromptLauncher( + promptInfo = promptInfo, + biometricPromptBuilder = biometricPromptBuilder + ) + .flatMapConcat { biometricResult: BiometricResult -> + when (biometricResult) { + BiometricSuccess -> { + authenticationUseCase.authenticateWithSecureElement(id, scope) + .map { result: AuthenticationState -> + when (result) { + AuthenticationState.AuthenticationFlowFinished -> IdpCommunicationSuccess + AuthenticationState.AuthenticationFlowInitialized -> IdpCommunicationStarted + AuthenticationState.IDPCommunicationFinished -> IdpCommunicationUpdated + AuthenticationState.IDPCommunicationAltAuthNotSuccessful -> AuthenticationNotSuccessful + AuthenticationState.IDPCommunicationFailed -> CommunicationFailure + AuthenticationState.IDPCommunicationInvalidCertificate -> InvalidCertificate + AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> InvalidOCSP + AuthenticationState.SecureElementCryptographyFailed -> SecureElementFailure + AuthenticationState.UserNotAuthenticated -> UserNotAuthenticated + else -> IllegalStateError(result) // should never happen + } + } + } + + else -> { + Napier.d { "Biometric authentication not success $biometricResult" } + flowOf(biometricResult) + } + } + } + } + + override fun removeAuthentication(id: ProfileIdentifier) { + controllerScope.launch { removeAuthenticationUseCase.invoke(id) } + } +} + +@Composable +fun rememberBiometricAuthenticator(): BiometricAuthenticator { + val activity = LocalContext.current as AppCompatActivity + val authenticationUseCase by rememberInstance() + val removeAuthenticationUseCase by rememberInstance() + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity) } + val biometricPromptInfo = biometricPromptBuilder.buildPromptInfoWithBestSecureOption( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info), + negativeButton = stringResource(R.string.auth_prompt_cancel) + ) + return remember { + BiometricAuthenticator( + authenticationUseCase = authenticationUseCase, + removeAuthenticationUseCase = removeAuthenticationUseCase, + promptInfo = biometricPromptInfo, + biometricPromptBuilder = biometricPromptBuilder + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt deleted file mode 100644 index bdd86214..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.authentication.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalPromptAuthenticator -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.ui.ExternalAuthenticationDialog -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import kotlinx.coroutines.launch - -@Composable -fun ExternalAuthPrompt( - authenticator: ExternalPromptAuthenticator -) { - val scope = rememberCoroutineScope() - val state = authenticator.state - val profile = authenticator.profile - - if (state is ExternalPromptAuthenticator.State.SelectInsurance) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties( - dismissOnBackPress = false, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - decorFitsSystemWindows = false - ) - ) { - Box( - Modifier - .semantics(false) { } - .fillMaxSize() - .imePadding() - .systemBarsPadding(), - contentAlignment = Alignment.BottomCenter - ) { - PromptScaffold( - title = stringResource(R.string.cdw_fasttrack_choose_insurance), - profile = profile, - onCancel = { - scope.launch { - authenticator.onCancel() - } - } - ) { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - ) { - OutlinedTextField( - value = state.authenticatorName, - onValueChange = {}, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(R.string.mini_cdw_fasttrack_search)) }, - shape = RoundedCornerShape(8.dp), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), - readOnly = true - ) - SpacerMedium() - PrimaryButtonSmall( - modifier = Modifier.align(Alignment.CenterHorizontally), - onClick = { - scope.launch { - authenticator.onInsuranceSelected() - } - } - ) { - Text(stringResource(R.string.mini_cdw_fasttrack_next)) - } - } - } - } - } - ExternalAuthenticationDialog() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt deleted file mode 100644 index d98aa74f..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.authentication.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.PrimaryButton - -private val PinRegex = """^\d{0,8}$""".toRegex() -private val PinCorrectRegex = """^\d{6,8}$""".toRegex() - -@Composable -internal fun HealthCardCredentials( - modifier: Modifier, - onNext: (pin: String) -> Unit -) { - var pin by remember { mutableStateOf("") } - var pinVisible by remember { mutableStateOf(false) } - val pinCorrect by remember { - derivedStateOf { pin.matches(PinCorrectRegex) } - } - - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) - ) { - Text( - stringResource(R.string.mini_cdw_intro_description), - style = AppTheme.typography.body2l - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = pin, - onValueChange = { - if (it.matches(PinRegex)) { - pin = it - } - }, - label = { Text(stringResource(R.string.mini_cdw_pin_input_label)) }, - placeholder = { Text(stringResource(R.string.mini_cdw_pin_input_placeholder)) }, - visualTransformation = if (pinVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.NumberPassword - ), - shape = RoundedCornerShape(8.dp), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), - keyboardActions = KeyboardActions { - onNext(pin) - }, - trailingIcon = { - IconToggleButton( - checked = pinVisible, - onCheckedChange = { pinVisible = it } - ) { - Icon( - if (pinVisible) { - Icons.Rounded.Visibility - } else { - Icons.Rounded.VisibilityOff - }, - null - ) - } - } - ) - PrimaryButton( - onClick = { onNext(pin) }, - enabled = pinCorrect, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.mini_cdw_pin_next)) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt deleted file mode 100644 index 55e10aa5..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.authentication.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.cardwall.mini.ui.SecureHardwarePromptAuthenticator -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.toAnnotatedString -import kotlinx.coroutines.launch - -@Composable -fun SecureHardwarePrompt( - authenticator: SecureHardwarePromptAuthenticator -) { - val scope = rememberCoroutineScope() - authenticator.showError?.let { error -> - val retryText = when (error) { - is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful -> Pair( - stringResource(R.string.cdw_mini_alt_auth_removed_title).toAnnotatedString(), - stringResource(R.string.cdw_mini_alt_auth_removed).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationFailed -> Pair( - stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), - stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidCertificate -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidOCSP -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString() - ) - } - - retryText.let { (title, message) -> - AcceptDialog( - header = title, - info = message, - acceptText = stringResource(R.string.ok), - onClickAccept = { - scope.launch { - if (error is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful) { - authenticator.removeAuthentication(error.profileId) - } - authenticator.resetErrorState() - } - } - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/AuthenticationFailureDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/AuthenticationFailureDialog.kt new file mode 100644 index 00000000..251fecab --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/AuthenticationFailureDialog.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.authentication.mapper.toDialogMapper +import de.gematik.ti.erp.app.authentication.model.AuthenticationDialogParameter +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import io.github.aakira.napier.Napier + +@Composable +fun AuthenticationFailureDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold +) { + event.listen { error -> + error.toDialogMapper()?.let { errorParams -> + dialogScaffold.show { + AuthenticationFailureDialog( + error = errorParams + ) { + it.dismiss() + } + } + } ?: run { + Napier.i { "No dialog parameters found for error: $error" } + } + } +} + +@Composable +private fun AuthenticationFailureDialog( + error: AuthenticationDialogParameter, + onClickAction: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(error.title), + body = stringResource(error.message), + onDismissRequest = onClickAction + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPrompt.kt new file mode 100644 index 00000000..05690f19 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPrompt.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.cardwall.mini.ui.BiometricPromptAuthenticator +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.AcceptDialog +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import kotlinx.coroutines.launch + +@Composable +fun BiometricPrompt( + authenticator: BiometricPromptAuthenticator +) { + val scope = rememberCoroutineScope() + authenticator.showError?.let { error -> + val retryText = when (error) { + is BiometricPromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful -> Pair( + stringResource(R.string.cdw_mini_alt_auth_removed_title).toAnnotatedString(), + stringResource(R.string.cdw_mini_alt_auth_removed).toAnnotatedString() + ) + + BiometricPromptAuthenticator.Error.RemoteCommunicationFailed -> Pair( + stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), + stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() + ) + + BiometricPromptAuthenticator.Error.RemoteCommunicationInvalidCertificate -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() + ) + + BiometricPromptAuthenticator.Error.RemoteCommunicationInvalidOCSP -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString() + ) + } + + retryText.let { (title, message) -> + AcceptDialog( + header = title, + info = message, + acceptText = stringResource(R.string.ok), + onClickAccept = { + scope.launch { + if (error is BiometricPromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful) { + authenticator.removeAuthentication(error.profileId) + } + authenticator.resetErrorState() + } + } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPromptLauncher.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPromptLauncher.kt new file mode 100644 index 00000000..e6079851 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/BiometricPromptLauncher.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import androidx.biometric.BiometricPrompt +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.BiometricResult.BiometricStarted +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult.BiometricResult.BiometricSuccess +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +fun biometricPromptLauncher( + promptInfo: BiometricPrompt.PromptInfo, + biometricPromptBuilder: BiometricPromptBuilder +): Flow = + callbackFlow { + launch { + send(BiometricStarted) + cancel() + } + val prompt = biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + trySendBlocking(BiometricSuccess) + }, + onError = { error, code -> + trySendBlocking( + AuthenticationResult.BiometricResult.BiometricError( + error = error, + errorCode = code + ) + ) + } + ) + prompt.authenticate(promptInfo) + awaitClose { prompt.cancelAuthentication() } + }.flowOn(Dispatchers.Main) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/EnrollBiometricDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/EnrollBiometricDialog.kt new file mode 100644 index 00000000..d0e78074 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/EnrollBiometricDialog.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Fingerprint +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.core.content.ContextCompat +import de.gematik.ti.erp.app.authentication.presentation.enrollBiometricsIntent +import de.gematik.ti.erp.app.authentication.presentation.enrollDeviceSecurityIntent +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +private fun EnrollBiometricDialog( + context: Context, + title: String, + body: String, + onDismiss: () -> Unit +) { + ErezeptAlertDialog( + title = title, + titleIcon = Icons.Rounded.Fingerprint, + bodyText = body, + confirmText = stringResource(R.string.enroll_biometric_dialog_settings), + dismissText = stringResource(R.string.enroll_biometric_dialog_cancel), + onConfirmRequest = { + ContextCompat.startActivity(context, enrollBiometricsIntent(), null) + onDismiss() + }, + onDismissRequest = onDismiss + ) +} + +@Composable +fun EnrollBiometricDialog( + event: ComposableEvent, + context: Context, + dialog: DialogScaffold, + title: String = stringResource(R.string.enroll_biometric_dialog_header), + body: String = stringResource(R.string.enroll_biometric_dialog_info) +) { + event.listen { + dialog.show { + EnrollBiometricDialog( + context = context, + title = title, + body = body, + onDismiss = { it.dismiss() } + ) + } + } +} + +@Composable +private fun EnrollDeviceSecurityDialog( + context: Context, + title: String, + body: String, + onDismiss: () -> Unit +) { + ErezeptAlertDialog( + title = title, + titleIcon = Icons.Rounded.Fingerprint, + bodyText = body, + confirmText = stringResource(R.string.enroll_biometric_dialog_settings), + dismissText = stringResource(R.string.enroll_biometric_dialog_cancel), + onConfirmRequest = { + ContextCompat.startActivity(context, enrollDeviceSecurityIntent(), null) + onDismiss() + }, + onDismissRequest = onDismiss + ) +} + +@Composable +fun EnrollDeviceSecurityDialog( + event: ComposableEvent, + context: Context, + dialog: DialogScaffold, + title: String = stringResource(R.string.enroll_device_security_dialog_header), + body: String = stringResource(R.string.enroll_device_security_dialog_info) +) { + event.listen { + dialog.show { + EnrollDeviceSecurityDialog( + context = context, + title = title, + body = body, + onDismiss = { it.dismiss() } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/ExternalAuthPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/ExternalAuthPrompt.kt new file mode 100644 index 00000000..2df7e0ce --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/ExternalAuthPrompt.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalPromptAuthenticator +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.ui.ExternalAuthenticationUiHandler +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall +import de.gematik.ti.erp.app.utils.compose.erezeptTextFieldColors +import kotlinx.coroutines.launch + +@Composable +fun ExternalAuthPrompt( + authenticator: ExternalPromptAuthenticator +) { + val scope = rememberCoroutineScope() + val state = authenticator.state + val profile = authenticator.profile + + if (state is ExternalPromptAuthenticator.State.SelectInsurance) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + decorFitsSystemWindows = false + ) + ) { + Box( + Modifier + .semantics(false) { } + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomCenter + ) { + PromptScaffold( + title = stringResource(R.string.cardwall_gid_header), + profile = profile, + onCancel = { + scope.launch { + authenticator.onCancel() + } + } + ) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + ) { + ErezeptOutlineText( + value = state.authenticatorName, + onValueChange = {}, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + label = { Text(stringResource(R.string.mini_cdw_fasttrack_search)) }, + shape = RoundedCornerShape(8.dp), + readOnly = true, + colors = erezeptTextFieldColors() + ) + SpacerMedium() + PrimaryButtonSmall( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + scope.launch { + authenticator.onInsuranceSelected() + } + } + ) { + Text(stringResource(R.string.mini_cdw_fasttrack_next)) + } + } + } + } + } + ExternalAuthenticationUiHandler() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardCredentials.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardCredentials.kt new file mode 100644 index 00000000..a3cce0d6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardCredentials.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.authentication.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.erezeptTextFieldColors +import de.gematik.ti.erp.app.utils.extensions.ErezeptKeyboardOptions + +private val PinRegex = """^\d{0,8}$""".toRegex() +private val PinCorrectRegex = """^\d{6,8}$""".toRegex() + +@Composable +internal fun HealthCardCredentials( + modifier: Modifier, + onNext: (pin: String) -> Unit +) { + var pin by remember { mutableStateOf("") } + var pinVisible by remember { mutableStateOf(false) } + val pinCorrect by remember { + derivedStateOf { pin.matches(PinCorrectRegex) } + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) + ) { + Text( + stringResource(R.string.mini_cdw_intro_description), + style = AppTheme.typography.body2l + ) + ErezeptOutlineText( + modifier = Modifier.fillMaxWidth(), + value = pin, + onValueChange = { + if (it.matches(PinRegex)) { + pin = it + } + }, + label = stringResource(R.string.mini_cdw_pin_input_label), + placeholder = stringResource(R.string.mini_cdw_pin_input_placeholder), + keyboardOptions = ErezeptKeyboardOptions.numberPassword, + visualTransformation = when { + pinVisible -> VisualTransformation.None + else -> PasswordVisualTransformation() + }, + keyboardActions = KeyboardActions { + onNext(pin) + }, + shape = RoundedCornerShape(SizeDefaults.one), + colors = erezeptTextFieldColors(), + trailingIcon = { + IconToggleButton( + checked = pinVisible, + onCheckedChange = { pinVisible = it } + ) { + Icon( + if (pinVisible) { + Icons.Rounded.Visibility + } else { + Icons.Rounded.VisibilityOff + }, + null + ) + } + } + ) + PrimaryButton( + onClick = { onNext(pin) }, + enabled = pinCorrect, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.mini_cdw_pin_next)) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardErrorDialog.kt similarity index 79% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardErrorDialog.kt index 72005401..6bb3ca47 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardErrorDialog.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.authentication.ui +package de.gematik.ti.erp.app.authentication.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource @@ -29,12 +29,17 @@ import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.toAnnotatedString @Requirement( - "A_20079", - "A_20085", - "A_20605#2", - sourceSpecification = "gemSpec_eRp_FdV", + "A_20079#1", + "A_20085#1", + "A_20605#4", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Display error messages from endpoint." ) +@Requirement( + "A_20605#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "The interceptor pattern is used to add the bearer token to the request." +) @Composable internal fun HealthCardErrorDialog( state: HealthCardPromptAuthenticator.State.ReadState.Error, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardPrompt.kt similarity index 92% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardPrompt.kt index 8a95a61a..54460214 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/HealthCardPrompt.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.authentication.ui +package de.gematik.ti.erp.app.authentication.ui.components import android.content.Intent import android.provider.Settings diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/PromptScaffold.kt similarity index 76% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/PromptScaffold.kt index 9ce8e229..0e3d4b00 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/components/PromptScaffold.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.authentication.ui +package de.gematik.ti.erp.app.authentication.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -38,12 +38,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.ui.Avatar +import de.gematik.ti.erp.app.profiles.ui.components.Avatar import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium @Composable fun PromptScaffold( @@ -74,8 +74,7 @@ fun PromptScaffold( modifier = Modifier.size(36.dp), emptyIcon = Icons.Rounded.PersonOutline, iconModifier = Modifier.size(20.dp), - profile = profile, - ssoStatusColor = null + profile = profile ) SpacerMedium() Column(modifier = Modifier.weight(1f)) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BackHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BackHandler.kt new file mode 100644 index 00000000..a93c68f3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BackHandler.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base + +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import io.github.aakira.napier.Napier + +@Composable +fun backPressedHandler( + canHandleBack: Boolean, + onBack: () -> Unit +): OnBackPressedCallback { + val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current + val callback = remember { + object : OnBackPressedCallback(canHandleBack) { + override fun handleOnBackPressed() { + if (canHandleBack) { + Napier.d { "on back pressed" } + // Only pop the back stack if backHandler is true + onBack() + } + } + } + } + + DisposableEffect(callback) { + backPressedDispatcher?.onBackPressedDispatcher?.addCallback(callback) + onDispose { + callback.remove() + } + } + return callback +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt index ae7eed50..12ab1f1e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("LongMethod", "MagicNumber") +@file:Suppress("LongMethod", "MagicNumber", "TooManyFunctions") package de.gematik.ti.erp.app.base @@ -37,7 +37,6 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.setMargins import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar @@ -47,12 +46,14 @@ import de.gematik.ti.erp.app.OlderSdkDomainVerifier import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.Sdk31DomainVerifier import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.app.ApplicationInnerPadding import de.gematik.ti.erp.app.appupdate.usecase.AppUpdateInfoUseCase import de.gematik.ti.erp.app.appupdate.usecase.ChangeAppUpdateFlagUseCase import de.gematik.ti.erp.app.appupdate.usecase.CheckVersionUseCase import de.gematik.ti.erp.app.appupdate.usecase.GetAppUpdateFlagUseCase import de.gematik.ti.erp.app.appupdate.usecase.GetAppUpdateManagerFlagUseCase import de.gematik.ti.erp.app.appupdate.usecase.GetAppUpdateManagerUseCase +import de.gematik.ti.erp.app.base.usecase.UpdateInAppMessageUseCase import de.gematik.ti.erp.app.core.IntentHandler import de.gematik.ti.erp.app.debugOverrides import de.gematik.ti.erp.app.demomode.DefaultDemoModeObserver @@ -61,6 +62,8 @@ import de.gematik.ti.erp.app.demomode.di.demoModeModule import de.gematik.ti.erp.app.demomode.di.demoModeOverrides import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.DefaultShowMedicationPlanSuccessScreenObserver +import de.gematik.ti.erp.app.medicationplan.ShowMedicationPlanSuccessObserver import de.gematik.ti.erp.app.timeouts.usecase.GetPauseMetricUseCase import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver @@ -70,6 +73,7 @@ import io.github.aakira.napier.Napier import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -83,13 +87,15 @@ import org.kodein.di.android.retainedSubDI import org.kodein.di.bindProvider import org.kodein.di.instance +private const val SNACKBAR_LINES = 3 + open class BaseActivity : SnackbarScaffold, DialogScaffold, DIAware, AppCompatActivity(), - DemoModeObserver by DefaultDemoModeObserver() { - + DemoModeObserver by DefaultDemoModeObserver(), + ShowMedicationPlanSuccessObserver by DefaultShowMedicationPlanSuccessScreenObserver() { override val di by retainedSubDI(closestDI(), copy = Copy.All) { // should be only done from feature module import(demoModeModule) @@ -97,6 +103,7 @@ open class BaseActivity : when { BuildConfig.DEBUG && BuildKonfig.INTERNAL -> { debugOverrides() + fullDescriptionOnError = true fullContainerTreeOnError = true } } @@ -111,47 +118,49 @@ open class BaseActivity : } private val checkVersionUseCase: CheckVersionUseCase by instance() - private val getAppUpdateManagerFlagUseCase: GetAppUpdateManagerFlagUseCase by instance() - private val appUpdateInfoUseCase: AppUpdateInfoUseCase by instance() - private val pauseTimeoutUseCase: GetPauseMetricUseCase by instance() - private val inactivityTimeoutObserver: InactivityTimeoutObserver by instance() - private val changeAppUpdateFlagUseCase: ChangeAppUpdateFlagUseCase by instance() - private val updateManagerUseCase: GetAppUpdateManagerUseCase by instance() - - val getAppUpdateFlagUseCase: GetAppUpdateFlagUseCase by instance() + private val updateInAppMessageUseCase: UpdateInAppMessageUseCase by instance() + private val _nfcTag = MutableSharedFlow() val analytics: Analytics by instance() - + val getAppUpdateFlagUseCase: GetAppUpdateFlagUseCase by instance() val intentHandler = IntentHandler(this@BaseActivity) - // This is needed to be declared here so that the dialog can be cancelled on-pause - private var dialog: Dialog? = null - - private val _nfcTag = MutableSharedFlow() + // This is needed to be declared here so that the dialog can be cancelled on-pause and when needed + var dialog: Dialog? = null private var pauseTimerHandler: Handler = Handler(Looper.getMainLooper()) + // this is a control variable to disable the zoom on screens which don't need them + val disableZoomTemporarily = MutableStateFlow(false) + + // this value is added to provide screens with the extra padding that they require since the app is inside a scaffold + var applicationInnerPadding: ApplicationInnerPadding? = null + /** * A [Runnable] that makes the app require an authentication */ @Requirement( - "O.Auth_7", - "O.Plat_12", + "O.Auth_8#2", + "O.Plat_9#3", sourceSpecification = "BSI-eRp-ePA", - rationale = "The LifeCycleState of the app is monitored. If the app stopped, the authentication " + - "process starts after a delay of 30 seconds. We opted for this delay for usability reasons, " + - "as selecting the profile picture and external authentication requires pausing the app." + rationale = "The timer forces the app to require authentication after a certain time of inactivity.", + codeLines = 5 ) private val pauseTimerRunnable = Runnable { inactivityTimeoutObserver.forceRequireAuthentication() } + @Requirement( + "O.Auth_8#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The timer to pause the app after 30 seconds is started when the app is paused." + ) override fun onPause() { super.onPause() // cancel the dialog when pausing since it might show up security concerns @@ -170,12 +179,10 @@ open class BaseActivity : inactivityTimeoutObserver.resetInactivityTimer() } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) lifecycleScope.launch { - intent?.let { - intentHandler.propagateIntent(it) - } + intentHandler.propagateIntent(intent) } } @@ -219,6 +226,7 @@ open class BaseActivity : @ColorRes backgroundTint: Int ) { val snackbar = Snackbar.make(findViewById(android.R.id.content), text, length) + snackbar.setTextMaxLines(SNACKBAR_LINES) val theme = ContextThemeWrapper( applicationContext, R.style.ThemeOverlay_MaterialAlertDialog_Rounded @@ -235,7 +243,7 @@ open class BaseActivity : width = CoordinatorLayout.LayoutParams.MATCH_PARENT height = CoordinatorLayout.LayoutParams.WRAP_CONTENT gravity = Gravity.BOTTOM - setMargins(24) + setMargins(24, 0, 24, 144) } snackbar.show() } @@ -277,6 +285,10 @@ open class BaseActivity : } } + suspend fun updateInAppMessage() { + updateInAppMessageUseCase.invoke() + } + // Flow on a non main thread to avoid ANR val nfcTagFlow: Flow get() = _nfcTag.onStart { @@ -284,6 +296,7 @@ open class BaseActivity : }.flowOn(Dispatchers.IO) companion object { + private fun BaseActivity.isNfcNotEnabled() = !NfcAdapter.getDefaultAdapter(this).isEnabled fun BaseActivity.throwExceptionOnNfcNotEnabled() { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ClipBoardCopy.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ClipBoardCopy.kt index 5e26240f..c379e8c7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ClipBoardCopy.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ClipBoardCopy.kt @@ -1,35 +1,39 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base import android.content.ClipData +import android.content.ClipboardManager import android.content.Context +import io.github.aakira.napier.Napier object ClipBoardCopy { fun copyToClipboard( context: Context, text: String ) { - val manager = context - .getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - + val manager = context.clipBoardManager + Napier.i { "Clipboard \n $text" } val clip = ClipData.newPlainText("url", text) manager.setPrimaryClip(clip) } } + +val Context.clipBoardManager: ClipboardManager + get() = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ComposableViewExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ComposableViewExtensions.kt index 2157279e..3b94aee7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ComposableViewExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/ComposableViewExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/Controller.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/Controller.kt new file mode 100644 index 00000000..3c55800c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/Controller.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base + +import androidx.compose.runtime.Stable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope + +/** + * A viewmodel renamed for naming consistency + */ +@Stable +abstract class Controller : ViewModel() { + val controllerScope = viewModelScope +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/CustomExceptions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/CustomExceptions.kt index 379ad3ee..55a0b501 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/CustomExceptions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/CustomExceptions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/DialogExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/DialogExtensions.kt index 2ed06d18..ed046271 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/DialogExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/DialogExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/FlowExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/FlowExtensions.kt new file mode 100644 index 00000000..6fd22504 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/FlowExtensions.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.firstOrNull + +val falseStateFlow + @Composable + get() = MutableStateFlow(false).collectAsState() + +/** + * Collects the results of a flow and calls the appropriate lambda based on the result type + */ +suspend fun Flow>.collectResult( + onSuccess: (value: T) -> R, + onFailure: (exception: Throwable) -> R +) { + collectLatest { result -> + result.fold( + onSuccess = onSuccess, + onFailure = onFailure + ) + } +} + +/** + * Updates the results of a flow to a UiState + */ +suspend fun Flow>.toUiState( + state: (UiState>) -> Unit +) { + runCatching { + this + }.fold( + onSuccess = { + val items = it.firstOrNull() + if (items?.size == 0) { + state(UiState.Empty()) + } else { + state(UiState.Data(items)) + } + }, + onFailure = { error -> + Napier.e { "Error on UsecaseResult load ${error.stackTraceToString()}" } + state(UiState.Error(error)) + } + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NetworkStatusTracker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NetworkStatusTracker.kt new file mode 100644 index 00000000..f95f6d1b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NetworkStatusTracker.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +class NetworkStatusTracker(context: Context) { + + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val networkStatus: Flow = callbackFlow { + + // Perform an initial network status check and emit the result immediately + val initialStatus = isNetworkAvailable() + trySend(initialStatus) + + val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(true) // Network is available + } + + override fun onLost(network: Network) { + trySend(false) // Network is lost + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + val hasInternetCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + trySend(hasInternetCapability) // Send the current network status + } + } + + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + }.distinctUntilChanged() // Emit only when the network status changes and on app start + + // Helper function to check the current network status + private fun isNetworkAvailable(): Boolean { + val network = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) + return networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true + } + + // public helper function to check the current network status + companion object { + fun NetworkStatusTracker.isNetworkAvailable(): Boolean { + return this.isNetworkAvailable() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NfcExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NfcExtensions.kt index 5c937d85..b04a70ba 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NfcExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NfcExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NonNullResult.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NonNullResult.kt new file mode 100644 index 00000000..344983c8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/NonNullResult.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base + +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent + +sealed class NonNullResult { + data class Success(val value: T) : NonNullResult() + class NullValue : NonNullResult() +} + +fun requireNonNull(input: T?): NonNullResult { + return if (input != null) { + NonNullResult.Success(input) + } else { + NonNullResult.NullValue() + } +} + +@Suppress("ComposableNaming") +@Composable +fun NonNullResult.fold( + onSuccess: @Composable (T) -> Unit, + onFailure: @Composable () -> Unit = { ErrorScreenComponent() } +) { + if (this is NonNullResult.Success) { + onSuccess(value) + } else { + onFailure() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SettingsIntent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SettingsIntent.kt index ef789e58..17d2c841 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SettingsIntent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SettingsIntent.kt @@ -1,39 +1,55 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.base +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.core.content.ContextCompat -fun Context.openSettingsAsNewActivity(action: String = Settings.ACTION_APPLICATION_DETAILS_SETTINGS) { +/** + * Open settings as new activity + * + * @param action: String. This specifies the action to be performed by the intent. + * @param isSimpleIntent: Boolean. Use [isSimpleIntent] = true if your call is something like + * context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + */ +fun Context.openSettingsAsNewActivity( + action: String, + isSimpleIntent: Boolean = false +) { try { - val uri = Uri.fromParts("package", packageName, null) - val intent = Intent(action) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.setData(uri) - ContextCompat.startActivity(this, intent, null) - } catch (e: Throwable) { + if (isSimpleIntent) { + val intent = Intent(action) + ContextCompat.startActivity(this, intent, null) + } else { + val uri = Uri.fromParts("package", packageName, null) + val intent = Intent(action) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setData(uri) + ContextCompat.startActivity(this, intent, null) + } + } catch (e: ActivityNotFoundException) { // General settings intent - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val intent = Intent(Settings.ACTION_APPLICATION_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ContextCompat.startActivity(this, intent, null) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SharedController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SharedController.kt deleted file mode 100644 index 6e566106..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/SharedController.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.base - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope - -/** - * SharedController is a ViewModel that can be used to share data between Composables. - * It is scoped to the lifecycle of the ViewModel. - * [controllerScope] is the scope that can be used to launch coroutines. - */ -open class SharedController : ViewModel() { - val controllerScope = viewModelScope -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/model/DownloadResourcesState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/model/DownloadResourcesState.kt new file mode 100644 index 00000000..f07dff4b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/model/DownloadResourcesState.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base.model + +enum class DownloadResourcesState { + NotStarted, + InProgress, + TasksDownloaded, + CommunicationsDownloaded, + InvoicesDownloaded, + Finished; + + companion object { + fun DownloadResourcesState.isFinished() = listOf(Finished, NotStarted).contains(this) + fun DownloadResourcesState.isInProgress() = this == InProgress + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetActiveProfileController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetActiveProfileController.kt new file mode 100644 index 00000000..b861fd08 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetActiveProfileController.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +open class GetActiveProfileController( + private val getActiveProfileUseCase: GetActiveProfileUseCase, + val onSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + val onFailure: ((Throwable, CoroutineScope) -> Unit)? = null +) : Controller() { + private val _activeProfile = MutableStateFlow>(UiState.Loading()) + private val _isRedemptionAllowed = MutableStateFlow(false) + + protected val onRefreshProfileAction = ComposableEvent() + + val activeProfile: StateFlow> = _activeProfile + val isRedemptionAllowed: StateFlow = _isRedemptionAllowed + + init { + initActiveProfile() + } + + fun refreshActiveProfile() { + initActiveProfile() + } + + private fun initActiveProfile() { + controllerScope.launch { + runCatching { + getActiveProfileUseCase.invoke() + .distinctUntilChanged { old, new -> + old.isSSOTokenValid() == new.isSSOTokenValid() || + old.isActive == new.isActive || + old.lastAuthenticated == new.lastAuthenticated || + old.color == new.color || + old.avatar == new.avatar || + old.image.contentEquals(new.image) || + old.ssoTokenScope?.token == new.ssoTokenScope?.token || + old.ssoTokenScope?.token?.token == new.ssoTokenScope?.token?.token + } + }.fold( + onSuccess = { profileFlow -> + val profile = profileFlow.first() + _isRedemptionAllowed.value = profile.isRedemptionAllowed() + _activeProfile.value = UiState.Data(profile) + onSuccess?.invoke(profile, controllerScope) + }, + onFailure = { + _activeProfile.value = UiState.Error(it) + onFailure?.invoke(it, controllerScope) + } + ) + onRefreshProfileAction.trigger(false) + } + } +} + +@Composable +fun rememberGetActiveProfileController( + onSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + onFailure: ((Throwable, CoroutineScope) -> Unit)? = null +): GetActiveProfileController { + val getActiveProfileUseCase by rememberInstance() + return remember { + GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = onSuccess, + onFailure = onFailure + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetProfileByIdController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetProfileByIdController.kt new file mode 100644 index 00000000..ad8e05e3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/presentation/GetProfileByIdController.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base.presentation + +import de.gematik.ti.erp.app.profiles.model.ProfileCombinedData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.letNotNullOnCondition +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +abstract class GetProfileByIdController( + private val selectedProfileId: ProfileIdentifier? = null, + private val getProfileByIdUseCase: GetProfileByIdUseCase, + private val getProfilesUseCase: GetProfilesUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + protected open val onSelectedProfileSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + protected open val onSelectedProfileFailure: ((Throwable, CoroutineScope) -> Unit)? = null, + protected open val onActiveProfileSuccess: ((ProfilesUseCaseData.Profile, CoroutineScope) -> Unit)? = null, + protected open val onActiveProfileFailure: ((Throwable, CoroutineScope) -> Unit)? = null +) : GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = onActiveProfileSuccess, + onFailure = onActiveProfileFailure +) { + private val _combinedProfile = MutableStateFlow>(UiState.Loading()) + val combinedProfile: StateFlow> = _combinedProfile + + init { + controllerScope.launch { + initCombinedProfile() + } + } + + fun refreshCombinedProfile() { + controllerScope.launch { + initCombinedProfile() + } + } + + val isSsoTokenValidForSelectedProfile: StateFlow by lazy { + combinedProfile.map { it.data?.selectedProfile?.ssoTokenScope } + .distinctUntilChanged() + .map { it?.token?.isValid() ?: false } + .stateIn( + scope = controllerScope, + started = SharingStarted.Eagerly, + initialValue = false + ) + } + + private suspend fun initCombinedProfile() { + runCatching { + _combinedProfile.value = UiState.Loading() + + val profiles = getProfilesUseCase().first() + val selectedProfile = selectedProfileId?.let { getProfileByIdUseCase(selectedProfileId).first() } + selectedProfile to profiles + }.fold( + onSuccess = { (selectedProfile, profiles) -> + selectedProfile?.let { onSelectedProfileSuccess?.invoke(it, controllerScope) } + letNotNullOnCondition( + first = selectedProfile, + condition = { profiles.isEmpty().not() } + ) { profile -> + _combinedProfile.value = UiState.Data( + ProfileCombinedData( + selectedProfile = profile, + profiles = profiles + ) + ) + } ?: run { + _combinedProfile.value = UiState.Data( + ProfileCombinedData( + selectedProfile = null, + profiles = profiles + ) + ) + } + }, + onFailure = { + onSelectedProfileFailure?.invoke(it, controllerScope) + _combinedProfile.value = UiState.Error(it) + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/DownloadAllResourcesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/DownloadAllResourcesUseCase.kt new file mode 100644 index 00000000..c6ef8035 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/DownloadAllResourcesUseCase.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base.usecase + +import de.gematik.ti.erp.app.base.NetworkStatusTracker +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.prescription.repository.DownloadResourcesStateRepository +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.CONFLATED +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException + +/** + * UseCase to download all resources for a given profile, + * including tasks, communications, invoices and new prescriptions count + */ + +class DownloadAllResourcesUseCase( + private val taskRepository: TaskRepository, + private val communicationRepository: CommunicationRepository, + private val invoicesRepository: InvoiceRepository, + private val profileRepository: ProfileRepository, + private val stateRepository: DownloadResourcesStateRepository, + private val networkStatusTracker: NetworkStatusTracker, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + suspend operator fun invoke(profileId: ProfileIdentifier): Result = + withContext(dispatcher) { + val resultChannel = Channel>() + try { + Napier.i { "Requesting download for $profileId" } + requestChannel.send( + Request( + resultChannel = resultChannel, + profileId = profileId + ) + ) + return@withContext resultChannel.receive() + } catch (cancelException: CancellationException) { + withContext(NonCancellable) { + resultChannel.close(cancelException) + } + return@withContext Result.failure(cancelException) + } + } + + private class Request( + val resultChannel: Channel>, + val profileId: ProfileIdentifier + ) + + private val scope = CoroutineScope(dispatcher) + + private val requestChannel = + Channel( + capacity = CONFLATED, + onUndeliveredElement = { + stateRepository.closeSnapshotState() + it.resultChannel.close(CancellationException()) + } + ) + + init { + scope.launch { + for (request in requestChannel) { + Napier.i { "Downloading queue has ${request.profileId}" } + handleDownloadRequest(request) + } + } + } + + private suspend fun handleDownloadRequest(request: Request) { + try { + val profileId = request.profileId + stateRepository.updateSnapshotState(DownloadResourcesState.NotStarted) + if (networkStatusTracker.networkStatus.first()) { // tell in progress only if nw is connected + with(stateRepository) { + updateSnapshotState(DownloadResourcesState.InProgress) + updateDetailState(DownloadResourcesState.InProgress) + } + } + + val newPrescriptionsCount = downloadTasks(profileId) + stateRepository.updateDetailState(DownloadResourcesState.TasksDownloaded) + + downloadCommunications(profileId) { + stateRepository.updateDetailState(DownloadResourcesState.CommunicationsDownloaded) + } + + if (profileRepository.checkIsProfilePKV(profileId)) { + Napier.i { "Downloaded invoices for $profileId" } + downloadInvoices(profileId) + stateRepository.updateDetailState(DownloadResourcesState.InvoicesDownloaded) + } + Napier.i { "Refresh finished for $profileId with $newPrescriptionsCount new prescriptions" } + request.resultChannel.send(Result.success(newPrescriptionsCount)) + } catch (e: CancellationException) { + Napier.e(e) { "CancellationException on downloading resources" } + request.resultChannel.close(e) + } catch (e: Exception) { + Napier.e(e) { "Error downloading resources" } + request.resultChannel.send(Result.failure(e)) + } finally { + with(stateRepository) { + updateSnapshotState(DownloadResourcesState.Finished) + updateDetailState(DownloadResourcesState.Finished) + } + } + } + + private suspend fun downloadTasks(profileId: ProfileIdentifier): Int = + withContext(NonCancellable) { + try { + taskRepository.downloadTasks(profileId).getOrThrow { throw it } + } catch (e: Exception) { + Napier.e(e) { "Error downloading tasks" } + throw e + } + } + + private suspend fun downloadCommunications( + profileId: ProfileIdentifier, + onComplete: () -> Unit + ) { + withContext(NonCancellable) { + try { + communicationRepository.downloadCommunications(profileId).getOrThrow { throw it } + } catch (e: Exception) { + Napier.e(e) { "Error downloading communications" } + throw e + } finally { + onComplete() + } + } + } + + private suspend fun downloadInvoices(profileId: ProfileIdentifier): Int = + withContext(NonCancellable) { + try { + invoicesRepository.downloadInvoices(profileId).getOrThrow { throw it } + } catch (e: Exception) { + Napier.e(e) { "Error downloading invoices" } + throw e + } + } + + private fun Result.getOrThrow(block: (Throwable) -> Nothing): T { + return getOrElse { + Napier.e(it) { "Error downloading resources" } + block(it) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/UpdateInAppMessageUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/UpdateInAppMessageUseCase.kt new file mode 100644 index 00000000..599ddced --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/usecase/UpdateInAppMessageUseCase.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.base.usecase + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.repository.InAppLocalMessageRepository +import de.gematik.ti.erp.app.messages.mappers.toEntity +import de.gematik.ti.erp.app.utils.extensions.toFormattedFloat +import io.realm.kotlin.ext.toRealmList +import kotlinx.coroutines.flow.first + +class UpdateInAppMessageUseCase( + private val inAppMessageRepository: InAppMessageRepository, + private val localMessageRepository: InAppLocalMessageRepository, + private val buildConfigInformation: BuildConfigInformation +) { + suspend operator fun invoke() { + val versionWithRC = buildConfigInformation.versionName() + val currentVersion = versionWithRC.toFormattedFloat() ?: 0.0f + val lastUpdatedVersion = inAppMessageRepository.lastUpdatedVersion.first()?.toFormattedFloat() ?: 0.0f + if ((lastUpdatedVersion + 1) < currentVersion) { + val internalMessages = localMessageRepository.getInternalMessages().first() + val newInternalMessage = internalMessages.filterMessagesEqualOrGreaterByVersion(lastUpdatedVersion) + val inAppLastVersion = newInternalMessage.last().version + if (newInternalMessage.isNotEmpty()) { + val newInAppMessages: List = newInternalMessage.map { + it.toEntity() + } + inAppMessageRepository.updateChangeLogs(newInAppMessages.toRealmList(), versionWithRC, inAppLastVersion) + } + } + } +} + +private fun List.filterMessagesEqualOrGreaterByVersion(currentVersion: Float): List { + return this.filter { message -> + (message.version.toFormattedFloat() ?: 0.0f) > currentVersion + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/di/CardUnlockModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/di/CardUnlockModule.kt index f56edbd6..41e4a904 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/di/CardUnlockModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/di/CardUnlockModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.di @@ -22,9 +22,10 @@ import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController import de.gematik.ti.erp.app.cardunlock.usecase.UnlockEgkUseCase import org.kodein.di.DI import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton import org.kodein.di.instance val cardUnlockModule = DI.Module("cardUnlockModule", allowSilentOverride = true) { bindProvider { UnlockEgkUseCase() } - bindProvider { CardUnlockGraphController(instance()) } + bindSingleton { CardUnlockGraphController(instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockGraph.kt index 7534146d..ad1c0624 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.navigation @@ -22,12 +22,12 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockCanScreen -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockEgkScreen -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockIntroScreen -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockNewSecretScreen -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockOldSecretScreen -import de.gematik.ti.erp.app.cardunlock.ui.CardUnlockPukScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockCanScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockEgkScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockIntroScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockNewSecretScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockOldSecretScreen +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockPukScreen import de.gematik.ti.erp.app.navigation.renderComposable import org.kodein.di.DI import org.kodein.di.instance diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockRoutes.kt index 9a97d21e..6c013d10 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.navigation @@ -26,13 +26,13 @@ import de.gematik.ti.erp.app.navigation.Routes object CardUnlockRoutes : NavigationRoutes { override fun subGraphName() = "cardUnlock" - const val UnlockMethod = "unlockMethod" + const val CARD_UNLOCK_NAV_UNLOCK_METHOD = "unlockMethod" object CardUnlockIntroScreen : Routes( NavigationRouteNames.CardUnlockIntroScreen.name, - navArgument(UnlockMethod) { type = NavType.StringType } + navArgument(CARD_UNLOCK_NAV_UNLOCK_METHOD) { type = NavType.StringType } ) { - fun path(unlockMethod: String) = path(UnlockMethod to unlockMethod) + fun path(unlockMethod: String) = path(CARD_UNLOCK_NAV_UNLOCK_METHOD to unlockMethod) } object CardUnlockCanScreen : Routes(NavigationRouteNames.CardUnlockCanScreen.name) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockScreen.kt index 8a628344..e7a70d92 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/navigation/CardUnlockScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/CardUnlockGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/CardUnlockGraphController.kt index 24a8375d..52fb61ce 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/CardUnlockGraphController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/CardUnlockGraphController.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.presentation import android.nfc.Tag -import de.gematik.ti.erp.app.base.SharedController +import de.gematik.ti.erp.app.base.Controller import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.card.model.command.UnlockMethod.None import de.gematik.ti.erp.app.cardunlock.usecase.UnlockEgkState @@ -35,7 +35,7 @@ import kotlinx.coroutines.launch class CardUnlockGraphController( private val unlockEgkUseCase: UnlockEgkUseCase -) : SharedController() { +) : Controller() { private val _unlockMethod = MutableStateFlow(None) private val _can = MutableStateFlow("") diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/ToggleUnlock.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/ToggleUnlock.kt index b0b5a5be..830084fa 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/ToggleUnlock.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/presentation/ToggleUnlock.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreen.kt deleted file mode 100644 index b581a7cc..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardunlock.ui - -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen -import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.cardwall.ui.CAN_LENGTH -import de.gematik.ti.erp.app.cardwall.ui.CanScreenContent -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode - -class CardUnlockCanScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardUnlockGraphController -) : CardUnlockScreen() { - @Composable - override fun Content() { - val lazyListState = rememberLazyListState() - - val unlockMethod by graphController.unlockMethod.collectAsStateWithLifecycle() - val can by graphController.can.collectAsStateWithLifecycle() - - CardWallScaffold( - modifier = Modifier.testTag(TestTag.CardWall.CAN.CANScreen), - backMode = NavigationBarMode.Back, - onBack = { - navController.popBackStack() - }, - title = when (unlockMethod) { - UnlockMethod.ChangeReferenceData -> stringResource(R.string.unlock_egk_top_bar_title_change_secret) - UnlockMethod.ResetRetryCounterWithNewSecret -> stringResource( - R.string.unlock_egk_top_bar_title_forgot_pin - ) - else -> stringResource(R.string.unlock_egk_top_bar_title) - }, - nextEnabled = can.length == CAN_LENGTH, - onNext = { - if (unlockMethod == UnlockMethod.ChangeReferenceData) { - navController.navigate( - CardUnlockRoutes.CardUnlockOldSecretScreen.path() - ) - } else { - navController.navigate( - CardUnlockRoutes.CardUnlockPukScreen.path() - ) - } - }, - listState = lazyListState, - nextText = stringResource(R.string.unlock_egk_next), - actions = { - TextButton(onClick = { - graphController.reset() - navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) - }) { - Text(stringResource(R.string.cancel)) - } - } - ) { innerPadding -> - CanScreenContent( - lazyListState = lazyListState, - innerPadding = innerPadding, - onClickLearnMore = { - navController.navigate(MainNavigationScreens.OrderHealthCard.path()) - }, - can = can, - onCanChange = graphController::setCardAccessNumber, - onNext = { - if (unlockMethod == UnlockMethod.ChangeReferenceData) { - navController.navigate( - CardUnlockRoutes.CardUnlockOldSecretScreen.path() - ) - } else { - navController.navigate( - CardUnlockRoutes.CardUnlockPukScreen.path() - ) - } - } - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreen.kt deleted file mode 100644 index 73b5e928..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreen.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardunlock.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.navOptions -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen -import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.ReadCardScreenComposable -import de.gematik.ti.erp.app.info.BuildConfigInformation -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes -import org.kodein.di.compose.rememberInstance - -class CardUnlockEgkScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardUnlockGraphController -) : CardUnlockScreen() { - @Composable - override fun Content() { - val unlockMethod by graphController.unlockMethod.collectAsStateWithLifecycle() - - val dialogState = rememberUnlockEgkDialogState() - val buildConfig by rememberInstance() - - val can by graphController.can.collectAsStateWithLifecycle() - val puk by graphController.puk.collectAsStateWithLifecycle() - val oldPin by graphController.oldPin.collectAsStateWithLifecycle() - val newPin by graphController.newPin.collectAsStateWithLifecycle() - - UnlockEgkDialog( - buildConfig = buildConfig, - unlockMethod = unlockMethod.name, - dialogState = dialogState, - graphController = graphController, - cardAccessNumber = can, - personalUnblockingKey = puk, - troubleShootingEnabled = true, - onClickTroubleshooting = { - navController.navigate(TroubleShootingRoutes.TroubleShootingIntroScreen.path()) - }, - oldPin = oldPin, - newPin = newPin, - onRetryCan = { - navController.navigate( - route = CardUnlockRoutes.CardUnlockCanScreen.route, - navOptions = navOptions { - popUpTo(CardUnlockRoutes.CardUnlockCanScreen.route) { - inclusive = true - } - } - ) - }, - onRetryOldSecret = { - navController.navigate( - route = CardUnlockRoutes.CardUnlockOldSecretScreen.route, - navOptions = navOptions { - popUpTo(CardUnlockRoutes.CardUnlockOldSecretScreen.route) { - inclusive = true - } - } - ) - }, - onRetryPuk = { - navController.navigate( - route = CardUnlockRoutes.CardUnlockPukScreen.route, - navOptions = navOptions { - popUpTo(CardUnlockRoutes.CardUnlockPukScreen.route) { - inclusive = true - } - } - ) - }, - onFinishUnlock = { - graphController.reset() - navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) - }, - onAssignPin = { - graphController.setUnlockMethodForGraph(UnlockMethod.ChangeReferenceData) - navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) - } - ) - ReadCardScreenComposable( - onBack = { navController.popBackStack() }, - onClickTroubleshooting = { - navController.navigate(TroubleShootingRoutes.TroubleShootingIntroScreen.path()) - } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreen.kt deleted file mode 100644 index 78a4521d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardunlock.ui - -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes.CardUnlockCanScreen -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen -import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SimpleCheck -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -class CardUnlockIntroScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardUnlockGraphController -) : CardUnlockScreen() { - - @Composable - override fun Content() { - val lazyListState = rememberLazyListState() - - val unlockMethod = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(CardUnlockRoutes.UnlockMethod) - ) - } - - LaunchedEffect(Unit) { - graphController.setUnlockMethodForGraph(UnlockMethod.valueOf(unlockMethod)) - } - - CardWallScaffold( - modifier = Modifier.testTag("unlockEgk/unlock"), - title = when (unlockMethod) { - UnlockMethod.ChangeReferenceData.name -> stringResource(R.string.unlock_egk_top_bar_title_change_secret) - UnlockMethod.ResetRetryCounterWithNewSecret.name -> stringResource( - R.string.unlock_egk_top_bar_title_forgot_pin - ) - - else -> stringResource(R.string.unlock_egk_top_bar_title) - }, - onBack = { - graphController.reset() - navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) - }, - onNext = { navController.navigate(CardUnlockCanScreen.path()) }, - nextText = stringResource(R.string.unlock_egk_next), - listState = lazyListState - ) { - CardUnlockIntroScreenContent(unlockMethod, lazyListState = lazyListState) - } - } -} - -@Composable -private fun CardUnlockIntroScreenContent( - unlockMethod: String, - lazyListState: LazyListState -) { - LazyColumn( - state = lazyListState, - modifier = Modifier.padding(PaddingDefaults.Medium) - ) { - item { - Text( - text = stringResource(R.string.unlock_egk_intro_what_you_need), - style = AppTheme.typography.h5 - ) - SpacerLarge() - SimpleCheck(stringResource(R.string.unlock_egk_intro_egk)) - if (unlockMethod == UnlockMethod.ChangeReferenceData.name) { - SimpleCheck(stringResource(R.string.unlock_egk_intro_pin)) - SpacerSmall() - Text( - text = stringResource(R.string.cdw_pin_info), - style = AppTheme.typography.caption1l - ) - } else { - SimpleCheck(stringResource(R.string.unlock_egk_intro_puk)) - SpacerSmall() - Text( - text = stringResource(R.string.unlock_egk_puk_info), - style = AppTheme.typography.caption1l - ) - } - } - } -} - -@LightDarkPreview -@Composable -fun CardUnlockIntroScreenContentResetRetryCounterPreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - CardUnlockIntroScreenContent( - unlockMethod = UnlockMethod.ResetRetryCounter.name, - lazyListState = lazyListState - ) - } -} - -@LightDarkPreview -@Composable -fun CardUnlockIntroScreenContentChangeReferenceDataPreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - CardUnlockIntroScreenContent( - unlockMethod = UnlockMethod.ChangeReferenceData.name, - lazyListState = lazyListState - ) - } -} - -@LightDarkPreview -@Composable -fun CardUnlockIntroScreenContentResetRetryCounterWithNewSecretPreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - CardUnlockIntroScreenContent( - unlockMethod = UnlockMethod.ResetRetryCounterWithNewSecret.name, - lazyListState = lazyListState - ) - } -} - -@LightDarkPreview -@Composable -fun CardUnlockIntroScreenContentNonePreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - CardUnlockIntroScreenContent( - unlockMethod = UnlockMethod.None.name, - lazyListState = lazyListState - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/components/UnlockEgkDialog.kt similarity index 96% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/components/UnlockEgkDialog.kt index be90928f..87c27958 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/components/UnlockEgkDialog.kt @@ -1,22 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.cardunlock.ui +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.cardunlock.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockCanScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockCanScreen.kt new file mode 100644 index 00000000..10df2299 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockCanScreen.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui.screens + +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen +import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallBottomBar +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.cardwall.ui.screens.CAN_LENGTH +import de.gematik.ti.erp.app.cardwall.ui.screens.CanScreenContent +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.CanBoolean +import de.gematik.ti.erp.app.utils.compose.preview.CanBooleanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold + +class CardUnlockCanScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardUnlockGraphController +) : CardUnlockScreen() { + @Composable + override fun Content() { + val lazyListState = rememberLazyListState() + + val unlockMethod by graphController.unlockMethod.collectAsStateWithLifecycle() + val can by graphController.can.collectAsStateWithLifecycle() + + CardWallScaffold( + modifier = Modifier.testTag(TestTag.CardWall.CAN.CANScreen), + backMode = NavigationBarMode.Back, + onBack = { + navController.popBackStack() + }, + title = when (unlockMethod) { + UnlockMethod.ChangeReferenceData -> stringResource(R.string.unlock_egk_top_bar_title_change_secret) + UnlockMethod.ResetRetryCounterWithNewSecret -> stringResource( + R.string.unlock_egk_top_bar_title_forgot_pin + ) + else -> stringResource(R.string.unlock_egk_top_bar_title) + }, + nextEnabled = can.length == CAN_LENGTH, + onNext = { + if (unlockMethod == UnlockMethod.ChangeReferenceData) { + navController.navigate( + CardUnlockRoutes.CardUnlockOldSecretScreen.path() + ) + } else { + navController.navigate( + CardUnlockRoutes.CardUnlockPukScreen.path() + ) + } + }, + listState = lazyListState, + nextText = stringResource(R.string.unlock_egk_next), + actions = { + TextButton(onClick = { + graphController.reset() + navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) + }) { + Text(stringResource(R.string.cancel)) + } + } + ) { innerPadding -> + CanScreenContent( + lazyListState = lazyListState, + innerPadding = innerPadding, + onClickLearnMore = { + navController.navigate(OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.path()) + }, + can = can, + onCanChange = graphController::setCardAccessNumber, + onNext = { + if (unlockMethod == UnlockMethod.ChangeReferenceData) { + navController.navigate( + CardUnlockRoutes.CardUnlockOldSecretScreen.path() + ) + } else { + navController.navigate( + CardUnlockRoutes.CardUnlockPukScreen.path() + ) + } + } + ) + } + } +} + +@LightDarkPreview +@Composable +fun CardUnlockCanScreenScaffoldPreview( + @PreviewParameter(CanBooleanPreviewParameterProvider::class) canNext: CanBoolean +) { + val lazyListState = rememberLazyListState() + + PreviewAppTheme { + TestScaffold( + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(R.string.unlock_egk_top_bar_title_change_secret), + + bottomBar = { + CardWallBottomBar( + onNext = {}, + nextEnabled = canNext.boolean, + nextText = stringResource(R.string.cdw_next) + ) + } + ) { + CanScreenContent( + lazyListState = lazyListState, + innerPadding = it, + onClickLearnMore = { }, + onNext = { }, + can = canNext.can, + onCanChange = { } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockEgkScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockEgkScreen.kt new file mode 100644 index 00000000..95e537c4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockEgkScreen.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui.screens + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen +import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController +import de.gematik.ti.erp.app.cardunlock.ui.components.UnlockEgkDialog +import de.gematik.ti.erp.app.cardunlock.ui.components.rememberUnlockEgkDialogState +import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallNfcPositionState +import de.gematik.ti.erp.app.cardwall.ui.components.ReadCardScreenScaffold +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreview +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreviewParameter +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import org.kodein.di.compose.rememberInstance + +class CardUnlockEgkScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardUnlockGraphController +) : CardUnlockScreen() { + @Composable + override fun Content() { + val unlockMethod by graphController.unlockMethod.collectAsStateWithLifecycle() + + val dialogState = rememberUnlockEgkDialogState() + val buildConfig by rememberInstance() + + val can by graphController.can.collectAsStateWithLifecycle() + val puk by graphController.puk.collectAsStateWithLifecycle() + val oldPin by graphController.oldPin.collectAsStateWithLifecycle() + val newPin by graphController.newPin.collectAsStateWithLifecycle() + + val nfcPositionState = rememberCardWallNfcPositionState() + val nfcPos = nfcPositionState.state.nfcData.nfcPos + + UnlockEgkDialog( + buildConfig = buildConfig, + unlockMethod = unlockMethod.name, + dialogState = dialogState, + graphController = graphController, + cardAccessNumber = can, + personalUnblockingKey = puk, + troubleShootingEnabled = true, + onClickTroubleshooting = { + navController.navigate(TroubleShootingRoutes.TroubleShootingIntroScreen.path()) + }, + oldPin = oldPin, + newPin = newPin, + onRetryCan = { + navController.navigate( + route = CardUnlockRoutes.CardUnlockCanScreen.route, + navOptions = navOptions { + popUpTo(CardUnlockRoutes.CardUnlockCanScreen.route) { + inclusive = true + } + } + ) + }, + onRetryOldSecret = { + navController.navigate( + route = CardUnlockRoutes.CardUnlockOldSecretScreen.route, + navOptions = navOptions { + popUpTo(CardUnlockRoutes.CardUnlockOldSecretScreen.route) { + inclusive = true + } + } + ) + }, + onRetryPuk = { + navController.navigate( + route = CardUnlockRoutes.CardUnlockPukScreen.route, + navOptions = navOptions { + popUpTo(CardUnlockRoutes.CardUnlockPukScreen.route) { + inclusive = true + } + } + ) + }, + onFinishUnlock = { + graphController.reset() + navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) + }, + onAssignPin = { + graphController.setUnlockMethodForGraph(UnlockMethod.ChangeReferenceData) + navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) + } + ) + ReadCardScreenScaffold( + onBack = { navController.popBackStack() }, + onClickTroubleshooting = { + navController.navigate(TroubleShootingRoutes.TroubleShootingIntroScreen.path()) + }, + nfcPosition = nfcPos + ) + } +} + +@LightDarkPreview +@Composable +fun CardUnlockEgkScreenPreview( + @PreviewParameter(NfcPositionPreviewParameter::class) previewData: NfcPositionPreview +) { + PreviewAppTheme { + ReadCardScreenScaffold( + onBack = {}, + onClickTroubleshooting = {}, + nfcPosition = previewData.nfcPos + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockIntroScreen.kt new file mode 100644 index 00000000..f9632d08 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockIntroScreen.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui.screens + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes.CardUnlockCanScreen +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen +import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SimpleCheck +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold +import de.gematik.ti.erp.app.utils.compose.preview.UnlockMethodPreviewParameterProvider + +class CardUnlockIntroScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardUnlockGraphController +) : CardUnlockScreen() { + + @Composable + override fun Content() { + val lazyListState = rememberLazyListState() + + val unlockMethod = remember { + requireNotNull( + navBackStackEntry.arguments?.getString(CardUnlockRoutes.CARD_UNLOCK_NAV_UNLOCK_METHOD) + ) + } + + LaunchedEffect(Unit) { + graphController.setUnlockMethodForGraph(UnlockMethod.valueOf(unlockMethod)) + } + + CardWallScaffold( + modifier = Modifier.testTag("unlockEgk/unlock"), + title = getTitle(unlockMethod), + onBack = { + graphController.reset() + navController.popBackStack(CardUnlockRoutes.CardUnlockIntroScreen.route, inclusive = true) + }, + onNext = { navController.navigate(CardUnlockCanScreen.path()) }, + nextText = stringResource(R.string.unlock_egk_next), + listState = lazyListState + ) { + CardUnlockIntroScreenContent(unlockMethod, lazyListState = lazyListState) + } + } +} + +@Composable +private fun CardUnlockIntroScreenContent( + unlockMethod: String, + lazyListState: LazyListState +) { + LazyColumn( + state = lazyListState, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) { + item { + Text( + text = stringResource(R.string.unlock_egk_intro_what_you_need), + style = AppTheme.typography.h5 + ) + SpacerLarge() + SimpleCheck(stringResource(R.string.unlock_egk_intro_egk)) + if (unlockMethod == UnlockMethod.ChangeReferenceData.name) { + SimpleCheck(stringResource(R.string.unlock_egk_intro_pin)) + SpacerSmall() + Text( + text = stringResource(R.string.cdw_pin_info), + style = AppTheme.typography.caption1l + ) + } else { + SimpleCheck(stringResource(R.string.unlock_egk_intro_puk)) + SpacerSmall() + Text( + text = stringResource(R.string.unlock_egk_puk_info), + style = AppTheme.typography.caption1l + ) + } + } + } +} + +/** + * This translation should be part of the UnlockMethod enum + */ +@Composable +private fun getTitle(unlockMethod: String) = when (unlockMethod) { + UnlockMethod.ChangeReferenceData.name -> stringResource(R.string.unlock_egk_top_bar_title_change_secret) + UnlockMethod.ResetRetryCounterWithNewSecret.name -> stringResource(R.string.unlock_egk_top_bar_title_forgot_pin) + else -> stringResource(R.string.unlock_egk_top_bar_title) +} + +@LightDarkPreview +@Composable +fun CardUnlockIntroScreenScaffoldPreview( + @PreviewParameter(UnlockMethodPreviewParameterProvider::class) unlockMethod: UnlockMethod +) { + val lazyListState = rememberLazyListState() + PreviewAppTheme { + TestScaffold( + topBarTitle = getTitle(unlockMethod = unlockMethod.name), + navigationMode = NavigationBarMode.Back + ) { + CardUnlockIntroScreenContent(unlockMethod.name, lazyListState) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockNewSecretScreen.kt similarity index 81% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockNewSecretScreen.kt index b345e9c7..3d8eda46 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockNewSecretScreen.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.cardunlock.ui +package de.gematik.ti.erp.app.cardunlock.ui.screens import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -30,21 +30,18 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ContentAlpha import androidx.compose.material.Icon import androidx.compose.material.IconToggleButton -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldColors -import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.TextFieldColors import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -55,6 +52,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController @@ -62,24 +60,30 @@ import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.cardwall.ui.PIN_RANGE -import de.gematik.ti.erp.app.cardwall.ui.PinInputField +import de.gematik.ti.erp.app.cardwall.ui.screens.PIN_RANGE +import de.gematik.ti.erp.app.cardwall.ui.screens.PinInputField import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText import de.gematik.ti.erp.app.utils.compose.HintCard import de.gematik.ti.erp.app.utils.compose.HintSmallImage import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.erezeptTextFieldColors +import de.gematik.ti.erp.app.utils.compose.preview.CardUnlockNewSecretScreenPreviewData +import de.gematik.ti.erp.app.utils.compose.preview.CardUnlockNewSecretScreenPreviewDataProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus const val InputFieldPosition = 2 + class CardUnlockNewSecretScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry, @@ -94,7 +98,6 @@ class CardUnlockNewSecretScreen( repeatedNewPin.isNotBlank() && newPin == repeatedNewPin } } - val lazyListState = rememberLazyListState() CardWallScaffold( modifier = Modifier.testTag("cardWall/secretScreen"), @@ -151,16 +154,8 @@ private fun CardUnlockNewSecretScreenContent( onRepeatedPinChange: (String) -> Unit, onNext: () -> Unit ) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } + val contentPadding by rememberContentPadding(innerPadding) + LazyColumn( state = lazyListState, modifier = Modifier.fillMaxSize(), @@ -245,7 +240,7 @@ fun ConformationPinInputField( val secretRegex = secretRegexString.toRegex() var secretVisible by remember { mutableStateOf(false) } - OutlinedTextField( + ErezeptOutlineText( modifier = modifier, value = repeatedPin, onValueChange = { @@ -253,7 +248,7 @@ fun ConformationPinInputField( onRepeatedPinChange(it) } }, - label = { Text(label) }, + label = label, visualTransformation = if (secretVisible) { VisualTransformation.None } else { @@ -267,11 +262,7 @@ fun ConformationPinInputField( shape = RoundedCornerShape(SizeDefaults.one), colors = if (repeatedPin.isEmpty()) { - TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ) + erezeptTextFieldColors(unfocusedLabelColor = AppTheme.colors.neutral400) } else { if (isConsistent) { textFieldColor(AppTheme.colors.green600) @@ -311,38 +302,36 @@ fun ConformationPinInputField( @Composable private fun textFieldColor(color: Color): TextFieldColors = - TextFieldDefaults.outlinedTextFieldColors( - focusedBorderColor = color.copy( + erezeptTextFieldColors( + focussedBorderColor = color.copy( alpha = ContentAlpha.high ), - focusedLabelColor = color.copy( + focussedLabelColor = color.copy( alpha = ContentAlpha.high ), unfocusedBorderColor = color.copy(alpha = ContentAlpha.high), unfocusedLabelColor = color.copy( alpha = ContentAlpha.high ), - trailingIconColor = color.copy( + focusedTrailingIconColor = color.copy( alpha = ContentAlpha.high ) ) @LightDarkPreview @Composable -fun SecretChangeLazyColumnPreview() { - val newSecret by rememberSaveable { mutableStateOf("") } - val repeatedNewSecret by remember { mutableStateOf("") } - val isConsistent = true +fun CardUnlockNewSecretScreenPreview( + @PreviewParameter(CardUnlockNewSecretScreenPreviewDataProvider::class) data: CardUnlockNewSecretScreenPreviewData +) { val lazyListState = rememberLazyListState() - val secretRange = PIN_RANGE PreviewAppTheme { CardUnlockNewSecretScreenContent( - newPin = newSecret, + newPin = data.pin, innerPadding = PaddingValues(PaddingDefaults.Medium), lazyListState = lazyListState, - repeatedNewPin = repeatedNewSecret, - isConsistent = isConsistent, - pinRange = secretRange, + repeatedNewPin = data.repeatedNewPin, + isConsistent = data.isConsistent, + pinRange = PIN_RANGE, onNewPinChange = {}, onRepeatedPinChange = {}, onNext = {} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockOldSecretScreen.kt similarity index 76% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockOldSecretScreen.kt index f82c6d4a..6e038d00 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockOldSecretScreen.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.cardunlock.ui +package de.gematik.ti.erp.app.cardunlock.ui.screens import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -27,12 +27,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController @@ -40,17 +39,20 @@ import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.cardwall.ui.PIN_RANGE -import de.gematik.ti.erp.app.cardwall.ui.PinInputField +import de.gematik.ti.erp.app.cardwall.ui.screens.PIN_RANGE +import de.gematik.ti.erp.app.cardwall.ui.screens.PinInputField import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.Pin +import de.gematik.ti.erp.app.utils.compose.preview.PinPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus class CardUnlockOldSecretScreen( override val navController: NavController, @@ -107,16 +109,8 @@ private fun CardUnlockOldSecretScreenContent( onOldPinChange: (String) -> Unit, onNext: () -> Unit ) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } + val contentPadding by rememberContentPadding(innerPadding) + LazyColumn( state = lazyListState, modifier = Modifier.fillMaxSize(), @@ -153,16 +147,16 @@ private fun CardUnlockOldSecretScreenContent( @LightDarkPreview @Composable -fun OldSecretScreenLazyColumnPreview() { +fun CardUnlockOldSecretScreenPreview( + @PreviewParameter(PinPreviewParameterProvider::class) pin: Pin +) { val lazyListState = rememberLazyListState() - val secretRange = PIN_RANGE - val oldSecret = "1234" // Sample oldSecret PreviewAppTheme { CardUnlockOldSecretScreenContent( lazyListState = lazyListState, - pinRange = secretRange, + pinRange = PIN_RANGE, innerPadding = PaddingValues(PaddingDefaults.Medium), - oldPin = oldSecret, + oldPin = pin.pin, onOldPinChange = {}, onNext = {} ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockPukScreen.kt similarity index 78% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockPukScreen.kt index 17e3e759..6fcdfe54 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/screens/CardUnlockPukScreen.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.cardunlock.ui +package de.gematik.ti.erp.app.cardunlock.ui.screens import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -27,19 +27,16 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController @@ -49,15 +46,18 @@ import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockScreen import de.gematik.ti.erp.app.cardunlock.presentation.CardUnlockGraphController import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PukPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus private const val PUK_LENGTH = 8 @@ -136,16 +136,8 @@ private fun PukScreenContent( onPukChange: (String) -> Unit, onNext: () -> Unit ) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } + val contentPadding by rememberContentPadding(innerPadding) + LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = contentPadding, @@ -179,7 +171,7 @@ private fun PukInputField( onNext: () -> Unit ) { val pukRegex = """^\d{0,$PUK_LENGTH}$""".toRegex() - OutlinedTextField( + ErezeptOutlineText( modifier = modifier .fillMaxWidth(), value = puk, @@ -188,18 +180,14 @@ private fun PukInputField( onPukChange(it) } }, - label = { Text(stringResource(R.string.unlock_egk_puk_label)) }, + label = stringResource(R.string.unlock_egk_puk_label), + placeholder = stringResource(R.string.unlock_egk_puk_label), keyboardOptions = KeyboardOptions( autoCorrect = false, keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Next ), shape = RoundedCornerShape(SizeDefaults.one), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), keyboardActions = KeyboardActions { if (puk.length == PUK_LENGTH) { onNext() @@ -215,12 +203,14 @@ private fun PukInputField( @LightDarkPreview @Composable -fun PukScreenPreview() { +fun CardUnlockPukScreenPreview( + @PreviewParameter(PukPreviewParameterProvider::class) puk: String +) { val lazyListState = rememberLazyListState() PreviewAppTheme { PukScreenContent( listState = lazyListState, - puk = "12345634", + puk = puk, innerPadding = PaddingValues(PaddingDefaults.Medium), onPukChange = {}, onNext = {} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt index 52ff5eb6..861370cf 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardunlock.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt index dfca40dd..6b8ce2a5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall @@ -33,5 +33,5 @@ val cardWallModule = DI.Module("cardWallModule") { bindProvider { CardWallLoadNfcPositionUseCase(instance()) } bindProvider { CardWallUseCase(instance(), instance()) } bindSingleton { MiniCardWallUseCase(instance(), instance()) } - bindProvider { CardWallGraphController() } + bindSingleton { CardWallGraphController(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt deleted file mode 100644 index 15454796..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.domain.biometric - -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import androidx.biometric.BiometricManager -import de.gematik.ti.erp.app.Requirement - -fun isDeviceSupportsBiometric(biometricMode: Int) = when (biometricMode) { - BiometricManager.BIOMETRIC_SUCCESS, - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> - true - else -> - false -} - -@Requirement( - "A_21583", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Check for support of BIOMETRIC_STRONG." -) -@Requirement( - "O.Biom_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check whether at least one biometric reference is available." -) -fun deviceStrongBiometricStatus(context: Context): Int { - val biometricManager = BiometricManager.from(context) - return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -} - -@Requirement( - "A_21578#2", - "A_21579#2", - "A_21580#2", - "A_21580#2", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Check for availability of strongbox." -) -@Requirement( - "O.Biom_2#2", - "O.Biom_3#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for availability of strongbox." -) -fun hasDeviceStrongBox(context: Context) = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) - } else { - false - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt index dac77b3e..f82de9a9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.mini.ui @@ -23,12 +23,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider +import de.gematik.ti.erp.app.authentication.model.Biometric import de.gematik.ti.erp.app.authentication.model.External import de.gematik.ti.erp.app.authentication.model.HealthCard import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData import de.gematik.ti.erp.app.authentication.model.None import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator -import de.gematik.ti.erp.app.authentication.model.SecureElement import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.core.IntentHandler import de.gematik.ti.erp.app.idp.model.HealthInsuranceData @@ -86,13 +86,13 @@ interface AuthenticationBridge { */ @Stable class Authenticator( - val authenticatorSecureElement: SecureHardwarePromptAuthenticator, + val authenticatorBiometric: BiometricPromptAuthenticator, val authenticatorHealthCard: HealthCardPromptAuthenticator, val authenticatorExternal: ExternalPromptAuthenticator, val mapper: PromptAuthenticationProvider, private val bridge: AuthenticationBridge ) { - fun authenticateForPrescriptions(profileId: ProfileIdentifier): Flow = + fun getAuthResult(profileId: ProfileIdentifier): Flow = flow { val initialAuthenticationData = bridge.authenticateFor(profileId) emitAll( @@ -102,7 +102,7 @@ class Authenticator( scope = PromptAuthenticator.AuthScope.Prescriptions, authenticators = listOf( authenticatorHealthCard, - authenticatorSecureElement, + authenticatorBiometric, authenticatorExternal ) ) @@ -116,8 +116,8 @@ class Authenticator( is HealthCard -> authenticatorHealthCard.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) - is SecureElement -> - authenticatorSecureElement.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) + is Biometric -> + authenticatorBiometric.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) is External -> authenticatorExternal.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) @@ -128,7 +128,7 @@ class Authenticator( } suspend fun cancelAllAuthentications() { - authenticatorSecureElement.cancelAuthentication() + authenticatorBiometric.cancelAuthentication() authenticatorHealthCard.cancelAuthentication() } } @@ -136,13 +136,13 @@ class Authenticator( @Composable fun rememberAuthenticator(intentHandler: IntentHandler): Authenticator { val bridge = rememberMiniCardWallController() - val promptSE = rememberSecureHardwarePromptAuthenticator(bridge) + val promptSE = rememberBiometricPromptAuthenticator(bridge) val promptHC = rememberHealthCardPromptAuthenticator(bridge) val promptEX = rememberExternalPromptAuthenticator(bridge, intentHandler) val mapper by rememberInstance() return remember { Authenticator( - authenticatorSecureElement = promptSE, + authenticatorBiometric = promptSE, authenticatorHealthCard = promptHC, authenticatorExternal = promptEX, bridge = bridge, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/BiometricPromptAuthenticator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/BiometricPromptAuthenticator.kt new file mode 100644 index 00000000..60f96b9f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/BiometricPromptAuthenticator.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.mini.ui + +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@Stable +class BiometricPromptAuthenticator( + private val bridge: AuthenticationBridge, + private val promptInfo: BiometricPrompt.PromptInfo, + private val biometricPromptBuilder: BiometricPromptBuilder +) : PromptAuthenticator { + private val cancelRequest = Channel(Channel.RENDEZVOUS) + + @Stable + sealed interface Error { + data object RemoteCommunicationFailed : Error + class RemoteCommunicationAltAuthNotSuccessful(val profileId: ProfileIdentifier) : Error + data object RemoteCommunicationInvalidCertificate : Error + data object RemoteCommunicationInvalidOCSP : Error + } + + var showError: Error? by mutableStateOf(null) + private set + + fun resetErrorState() { + showError = null + } + + suspend fun removeAuthentication(profileId: ProfileIdentifier) { + bridge.doRemoveAuthentication(profileId) + } + + override fun authenticate( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope + ): Flow = callbackFlow { + launch { + cancelRequest.receive() + send(PromptAuthenticator.AuthResult.Cancelled) + cancel() + } + val prompt = biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + trySendBlocking(PromptAuthenticator.AuthResult.Authenticated) + channel.close() + }, + onError = { _: String, _: Int -> + trySendBlocking(PromptAuthenticator.AuthResult.Cancelled) + channel.close() + } + ) + + prompt.authenticate(promptInfo) + + awaitClose { + prompt.cancelAuthentication() + } + }.flowOn(Dispatchers.Main) + .map { authResult: PromptAuthenticator.AuthResult -> + if (authResult == PromptAuthenticator.AuthResult.Authenticated) { + bridge.doSecureElementAuthentication( + profileId = profileId, + scope = scope + ).first { authState -> + authState.isFailure() || authState.isFinal() + }.let { authState -> + when { + authState.isNotAuthenticatedFailure() -> + PromptAuthenticator.AuthResult.UserNotAuthenticated + + authState.isFailure() -> { + showError = when (authState) { + AuthenticationState.IDPCommunicationAltAuthNotSuccessful -> Error.RemoteCommunicationAltAuthNotSuccessful(profileId) + AuthenticationState.IDPCommunicationFailed -> Error.RemoteCommunicationFailed + AuthenticationState.IDPCommunicationInvalidCertificate -> Error.RemoteCommunicationInvalidCertificate + AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> Error.RemoteCommunicationInvalidOCSP + else -> null + } + PromptAuthenticator.AuthResult.Cancelled + } + + authState.isFinal() -> + PromptAuthenticator.AuthResult.Authenticated + + else -> + error("unreachable") + } + } + } else { + authResult + } + } + + override suspend fun cancelAuthentication() { + cancelRequest.trySend(Unit) + } +} + +@Composable +fun rememberBiometricPromptAuthenticator( + bridge: AuthenticationBridge +): BiometricPromptAuthenticator { + val activity = LocalContext.current as AppCompatActivity + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity) } + val biometricPromptInfo = biometricPromptBuilder.buildPromptInfoWithBestSecureOption( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info), + negativeButton = stringResource(R.string.auth_prompt_cancel) + ) + return remember { + BiometricPromptAuthenticator(bridge, biometricPromptInfo, biometricPromptBuilder) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalPromptAuthenticator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalPromptAuthenticator.kt index c3c27999..c1429058 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalPromptAuthenticator.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalPromptAuthenticator.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.mini.ui diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt index 7475e899..419054c0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.mini.ui @@ -146,17 +146,16 @@ class HealthCardPromptAuthenticator( } @Requirement( - "A_19937", - "A_20079", - "A_20605#1", - sourceSpecification = "gemSpec_eRp_FdV", + "A_20605#2", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Propagates IDP auth states to the user." ) @Requirement( - "GS-A_5542#1", - sourceSpecification = "gemSpec_Krypt", + "A_19937#3", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Propagates IDP auth states to the user." ) + @Suppress("CyclomaticComplexMethod") private fun AuthenticationState.emitAuthState() { when { isInProgress() -> { @@ -177,6 +176,16 @@ class HealthCardPromptAuthenticator( } } + @Requirement( + "A_20079#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Propagates IDP auth states to the user." + ) + @Requirement( + "A_20605#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Decoding errors as RemoteCommunication errors." + ) isFailure() -> { state = when (this) { AuthenticationState.HealthCardCommunicationInterrupted -> diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt index 65d3125e..d9ff79b0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.mini.ui @@ -23,12 +23,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.authentication.model.Biometric import de.gematik.ti.erp.app.authentication.model.External import de.gematik.ti.erp.app.authentication.model.HealthCard import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData import de.gematik.ti.erp.app.authentication.model.None import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator -import de.gematik.ti.erp.app.authentication.model.SecureElement import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcHealthCard import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase @@ -79,7 +79,7 @@ class MiniCardWallController( ): InitialAuthenticationData { val profile = miniCardWallUseCase.profileData(profileId).first() return when ( - val ssoTokenScope = miniCardWallUseCase + val ssoTokenScope: IdpData.SingleSignOnTokenScope? = miniCardWallUseCase .authenticationData(profileId).first().singleSignOnTokenScope ) { is IdpData.ExternalAuthenticationToken -> External( @@ -89,7 +89,7 @@ class MiniCardWallController( ) is IdpData.AlternateAuthenticationToken, - is IdpData.AlternateAuthenticationWithoutToken -> SecureElement(profile = profile) + is IdpData.AlternateAuthenticationWithoutToken -> Biometric(profile = profile) is IdpData.DefaultToken -> HealthCard( can = ssoTokenScope.cardAccessNumber, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt deleted file mode 100644 index d4316fc7..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.mini.ui - -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity -import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator -import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.github.aakira.napier.Napier -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -@Stable -class SecureHardwarePromptAuthenticator( - val activity: FragmentActivity, - private val bridge: AuthenticationBridge, - private val promptInfo: BiometricPrompt.PromptInfo -) : PromptAuthenticator { - private val executor = ContextCompat.getMainExecutor(activity) - private val cancelRequest = Channel(Channel.RENDEZVOUS) - - @Stable - sealed interface Error { - object RemoteCommunicationFailed : Error - class RemoteCommunicationAltAuthNotSuccessful(val profileId: ProfileIdentifier) : Error - object RemoteCommunicationInvalidCertificate : Error - object RemoteCommunicationInvalidOCSP : Error - } - - var showError: Error? by mutableStateOf(null) - private set - - fun resetErrorState() { - showError = null - } - - suspend fun removeAuthentication(profileId: ProfileIdentifier) { - bridge.doRemoveAuthentication(profileId) - } - - override fun authenticate( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope - ): Flow = callbackFlow { - launch { - cancelRequest.receive() - send(PromptAuthenticator.AuthResult.Cancelled) - cancel() - } - - val prompt = BiometricPrompt( - activity, - executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - - trySendBlocking(PromptAuthenticator.AuthResult.Authenticated) - - channel.close() - } - - override fun onAuthenticationError( - errCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errCode, errString) - - Napier.e("Failed to authenticate: $errString") - - trySendBlocking(PromptAuthenticator.AuthResult.Cancelled) - - channel.close() - } - } - ) - - prompt.authenticate(promptInfo) - - awaitClose { - prompt.cancelAuthentication() - } - }.flowOn(Dispatchers.Main) - .map { - if (it == PromptAuthenticator.AuthResult.Authenticated) { - bridge.doSecureElementAuthentication( - profileId = profileId, - scope = scope - ).first { authState -> - authState.isFailure() || authState.isFinal() - }.let { authState -> - when { - authState.isNotAuthenticatedFailure() -> - PromptAuthenticator.AuthResult.UserNotAuthenticated - - authState.isFailure() -> { - showError = when (authState) { - AuthenticationState.IDPCommunicationAltAuthNotSuccessful -> - Error.RemoteCommunicationAltAuthNotSuccessful(profileId) - AuthenticationState.IDPCommunicationFailed -> - Error.RemoteCommunicationFailed - AuthenticationState.IDPCommunicationInvalidCertificate -> - Error.RemoteCommunicationInvalidCertificate - AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> - Error.RemoteCommunicationInvalidOCSP - else -> null - } - PromptAuthenticator.AuthResult.Cancelled - } - - authState.isFinal() -> - PromptAuthenticator.AuthResult.Authenticated - - else -> - error("unreachable") - } - } - } else { - it - } - } - - override suspend fun cancelAuthentication() { - cancelRequest.trySend(Unit) - } -} - -@Composable -fun rememberSecureHardwarePromptAuthenticator( - bridge: AuthenticationBridge -): SecureHardwarePromptAuthenticator { - val activity = LocalContext.current as FragmentActivity - val title = stringResource(R.string.alternate_auth_header) - val description = stringResource(R.string.alternate_auth_info) - val negativeButton = stringResource(R.string.cancel) - val promptInfo = remember { - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .setNegativeButtonText(negativeButton) - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG - ) - .build() - } - return remember { - SecureHardwarePromptAuthenticator(activity, bridge, promptInfo) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt index 40ff4cf2..277852d5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.card diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt index de94194a..c59ab325 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.card diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt index 34676afa..419e5d1f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.card diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallGraph.kt index fa57d3f3..f03ea733 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.navigation @@ -22,13 +22,14 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.ui.CardWallCanScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallExternalAuthenticationScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallIntroScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallPinScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallReadCardScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallSaveCredentialsInfoScreen -import de.gematik.ti.erp.app.cardwall.ui.CardWallSaveCredentialsScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallCanScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallGidHelpScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallGidListScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallIntroScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallPinScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallReadCardScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallSaveCredentialsInfoScreen +import de.gematik.ti.erp.app.cardwall.ui.screens.CardWallSaveCredentialsScreen import de.gematik.ti.erp.app.navigation.renderComposable import org.kodein.di.DI import org.kodein.di.instance @@ -105,10 +106,19 @@ fun NavGraphBuilder.cardWallGraph( ) } renderComposable( - route = CardWallRoutes.CardWallExternalAuthenticationScreen.route, - arguments = CardWallRoutes.CardWallExternalAuthenticationScreen.arguments + route = CardWallRoutes.CardWallGidListScreen.route, + arguments = CardWallRoutes.CardWallGidListScreen.arguments ) { navEntry -> - CardWallExternalAuthenticationScreen( + CardWallGidListScreen( + navController = navController, + navBackStackEntry = navEntry, + graphController = controller + ) + } + renderComposable( + route = CardWallRoutes.CardWallGidHelpScreen.route + ) { navEntry -> + CardWallGidHelpScreen( navController = navController, navBackStackEntry = navEntry, graphController = controller diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallRoutes.kt index ee93d121..05a6feed 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallRoutes.kt @@ -1,42 +1,81 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.navigation +import androidx.navigation.NavBackStackEntry import androidx.navigation.navArgument +import de.gematik.ti.erp.app.authentication.model.GidEventData import de.gematik.ti.erp.app.navigation.NavigationRouteNames import de.gematik.ti.erp.app.navigation.NavigationRoutes import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.navigation.fromNavigationString +import de.gematik.ti.erp.app.navigation.toNavigationString import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier object CardWallRoutes : NavigationRoutes { override fun subGraphName() = "cardwall" - const val profileId = "profileId" + const val CARD_WALL_NAV_PROFILE_ID = "profileId" + const val CARD_WALL_PIN_NAV_PROFILE_ID = "cardwall-profileId" + const val GID_AUTH_INFORMATION = "gidAuthInformation" + const val CARD_WALL_NAV_CAN = "cardwall-can" + object CardWallIntroScreen : Routes( NavigationRouteNames.CardWallIntroScreen.name, - navArgument(profileId) { type = androidx.navigation.NavType.StringType } + navArgument(CARD_WALL_NAV_PROFILE_ID) { type = androidx.navigation.NavType.StringType }, + navArgument(GID_AUTH_INFORMATION) { type = androidx.navigation.NavType.StringType } ) { - fun path(profileIdentifier: ProfileIdentifier) = path(profileId to profileIdentifier) + fun path(profileIdentifier: ProfileIdentifier) = path( + CARD_WALL_NAV_PROFILE_ID to profileIdentifier, + GID_AUTH_INFORMATION to "" + ) + + fun pathWithGid( + profileIdentifier: ProfileIdentifier, + gidEventData: GidEventData + ) = path( + CARD_WALL_NAV_PROFILE_ID to profileIdentifier, + GID_AUTH_INFORMATION to gidEventData.toNavigationString() + ) } + object CardWallCanScreen : Routes(NavigationRouteNames.CardWallCanScreen.name) - object CardWallPinScreen : Routes(NavigationRouteNames.CardWallPinScreen.name) + object CardWallPinScreen : Routes( + NavigationRouteNames.CardWallPinScreen.name, + navArgument(CARD_WALL_NAV_CAN) { type = androidx.navigation.NavType.StringType }, + navArgument(CARD_WALL_PIN_NAV_PROFILE_ID) { type = androidx.navigation.NavType.StringType } + ) { + fun path( + profileIdentifier: ProfileIdentifier, + can: String + ) = path( + CARD_WALL_PIN_NAV_PROFILE_ID to profileIdentifier, + CARD_WALL_NAV_CAN to can + ) + } object CardWallSaveCredentialsScreen : Routes(NavigationRouteNames.CardWallSaveCredentialsScreen.name) object CardWallSaveCredentialsInfoScreen : Routes(NavigationRouteNames.CardWallSaveCredentialsInfoScreen.name) object CardWallReadCardScreen : Routes(NavigationRouteNames.CardWallReadCardScreen.name) - object CardWallExternalAuthenticationScreen : Routes(NavigationRouteNames.CardWallExternalAuthenticationScreen.name) + object CardWallGidListScreen : Routes(NavigationRouteNames.CardWallGidListScreen.name) + object CardWallGidHelpScreen : Routes(NavigationRouteNames.CardWallGidHelpScreen.name) + + fun NavBackStackEntry.processGidEventData(): GidEventData? { + val authInfo = arguments?.getString(GID_AUTH_INFORMATION) + return authInfo?.takeIf { it.isNotEmpty() }?.let { fromNavigationString(it) } + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallScreen.kt index bb1810db..dbd4e27f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/navigation/CardWallScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/AltPairing.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/AltPairing.kt deleted file mode 100644 index 2886fb67..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/AltPairing.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.presentation - -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import androidx.annotation.RequiresApi -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.secureRandomInstance -import io.github.aakira.napier.Napier -import kotlinx.coroutines.suspendCancellableCoroutine -import org.bouncycastle.util.encoders.Base64 -import java.security.KeyPairGenerator -import java.security.KeyStore -import java.security.KeyStoreException -import java.security.PublicKey -import java.security.spec.ECGenParameterSpec -import kotlin.coroutines.resume - -private const val KeyStoreAliasKeySize = 32 // in bytes -private const val KeyTimeout = 15 * 60 // in seconds - -@Stable -class AltPairingProvider( - private val activity: FragmentActivity, - private val promptInfo: BiometricPrompt.PromptInfo -) { - private val executor = ContextCompat.getMainExecutor(activity) - - sealed interface AuthResult { - object Error : AuthResult - object Authenticated : AuthResult - - @Stable - class Initialized(val aliasOfSecureElementEntry: ByteArray, val publicKey: PublicKey) : AuthResult - } - - @Requirement( - "A_21576#1", - "A_21578#1", - "A_21579#1", - "A_21580#1", - "A_21580#1", - "A_21581", - "A_21585", - "A_21586", - "A_21587", - "A_21588", - "A_21589", - "A_21590", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Initialize biometric authentication for strongbox backed devices." - ) - @Requirement( - "O.Biom_1#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Initialize biometric authentication for strongbox backed devices." - ) - @RequiresApi(Build.VERSION_CODES.P) - suspend fun initializeAndPrompt(): AuthResult = suspendCancellableCoroutine { continuation -> - val aliasOfSecureElementEntry = ByteArray(KeyStoreAliasKeySize).apply { - secureRandomInstance().nextBytes(this) - } - - @Requirement( - "O.Biom_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The app uses the Android keystore to evaluate the biometric authentication" - ) - val keyPairGenerator = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_EC, - "AndroidKeyStore" - ) - - val parameterSpec = KeyGenParameterSpec.Builder( - Base64.toBase64String(aliasOfSecureElementEntry), - KeyProperties.PURPOSE_SIGN - ).apply { - @Requirement( - "O.Biom_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Biometric secured private keys are invalid whenever the biometrics setup changes. " + - "Invalidates biometry after changes" - ) - setInvalidatedByBiometricEnrollment(true) - setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // While the documentation of Android suggests to set this to zero, this is safe to use. - // If the key is used and later, e.g. a fingerprint is added, the keystore implementation of - // Android will throw a `KeyPermanentlyInvalidatedException`. Later on if the user restarts - // the phone, the key is permanently invalidated and the actual `UserNotAuthenticatedException` - // is thrown. - @Requirement( - "O.Biom_2#3", - "O.Biom_3#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Require Biometric STRONG." - ) - setUserAuthenticationParameters(KeyTimeout, KeyProperties.AUTH_BIOMETRIC_STRONG) - } - setIsStrongBoxBacked(true) - setDigests(KeyProperties.DIGEST_SHA256) - - setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) - }.build() - - keyPairGenerator.initialize(parameterSpec) - val keyPair = keyPairGenerator.generateKeyPair() - val publicKey = keyPair.public // required to init - - val prompt = BiometricPrompt( - activity, - executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - - continuation.resume( - AuthResult.Initialized( - aliasOfSecureElementEntry = aliasOfSecureElementEntry, - publicKey = publicKey - ) - ) - } - - override fun onAuthenticationError( - errCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errCode, errString) - - Napier.e("Failed to authenticate: $errString") - - cleanup(aliasOfSecureElementEntry) - continuation.resume(AuthResult.Error) - } - } - ) - - prompt.authenticate(promptInfo) - - continuation.invokeOnCancellation { - prompt.cancelAuthentication() - cleanup(aliasOfSecureElementEntry) - } - } - - fun cleanup(aliasOfSecureElementEntry: ByteArray) { - try { - KeyStore.getInstance("AndroidKeyStore") - .apply { load(null) } - .deleteEntry(Base64.toBase64String(aliasOfSecureElementEntry)) - } catch (e: KeyStoreException) { - Napier.e("Couldn't remove key from keystore on failure; expected to happen.", e) - } - } -} - -@Composable -fun rememberAltPairingProvider(): AltPairingProvider { - val activity = LocalContext.current as FragmentActivity - val title = stringResource(R.string.alternate_auth_header) - val description = stringResource(R.string.alternate_auth_info) - val negativeButton = stringResource(R.string.cancel) - val promptInfo = remember { - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .setNegativeButtonText(negativeButton) - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG - ) - .build() - } - return remember { - AltPairingProvider(activity, promptInfo) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallController.kt index 99633e10..ab7b7ffb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.presentation @@ -24,8 +24,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.DomainVerifier +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.Controller import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcHealthCard import de.gematik.ti.erp.app.cardwall.ui.components.CardWallAuthenticationData import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState @@ -34,6 +35,7 @@ import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import io.github.aakira.napier.Napier +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -44,9 +46,9 @@ import org.kodein.di.compose.rememberInstance class CardWallController( private val cardWallUseCase: CardWallUseCase, private val authenticationUseCase: AuthenticationUseCase, - private val dispatchers: DispatchProvider, private val domainVerifier: DomainVerifier -) { +) : Controller() { + fun doAuthentication( profileId: ProfileIdentifier, authenticationData: CardWallAuthenticationData, @@ -55,7 +57,7 @@ class CardWallController( val cardChannel = tag.map { NfcHealthCard.connect(it) } return when (authenticationData) { - is CardWallAuthenticationData.AltPairingWithHealthCard -> + is CardWallAuthenticationData.SaveCredentialsWithHealthCard -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { authenticationUseCase.pairDeviceWithHealthCardAndSecureElement( profileId = profileId, @@ -80,6 +82,11 @@ class CardWallController( } is CardWallAuthenticationData.HealthCard -> + @Requirement( + "O.Auth_3#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Implementation of the EGK connection" + ) authenticationUseCase.authenticateWithHealthCard( profileId = profileId, can = authenticationData.cardAccessNumber, @@ -87,11 +94,11 @@ class CardWallController( cardChannel = cardChannel ) } - .flowOn(dispatchers.io) + .flowOn(Dispatchers.IO) } - fun checkNfcEnabled(): Boolean { - return cardWallUseCase.checkNfcEnabled() + val isNfcEnabled: Boolean by lazy { + cardWallUseCase.isNfcEnabled() } val isDomainVerified by lazy { domainVerifier.areDomainsVerified } @@ -106,13 +113,11 @@ fun rememberCardWallController(): CardWallController { val cardWallUseCase by rememberInstance() val authenticationUseCase by rememberInstance() val domainVerifier by rememberInstance() - val dispatchers by rememberInstance() return remember { CardWallController( cardWallUseCase = cardWallUseCase, authenticationUseCase = authenticationUseCase, - domainVerifier = domainVerifier, - dispatchers = dispatchers + domainVerifier = domainVerifier ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallGraphController.kt index b0f63e3e..29278bd5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallGraphController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallGraphController.kt @@ -1,50 +1,77 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.presentation -import de.gematik.ti.erp.app.base.SharedController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.authentication.model.GidEventData +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.idp.model.UniversalLinkIdp +import de.gematik.ti.erp.app.idp.model.error.GematikResponseError +import de.gematik.ti.erp.app.idp.usecase.GetUniversalLinkForHealthInsuranceAppsUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.net.URI -class CardWallGraphController : SharedController() { +class CardWallGraphController( + private val getUniversalLinkUseCase: GetUniversalLinkForHealthInsuranceAppsUseCase, + private val switchProfileToPKVUseCase: SwitchProfileToPKVUseCase +) : Controller() { private val _profileId = MutableStateFlow("") private val _can = MutableStateFlow("") private val _pin = MutableStateFlow("") - private val _altPairing: MutableStateFlow = MutableStateFlow(null) + private val _saveCredentials: MutableStateFlow = MutableStateFlow(null) val profileId: StateFlow = _profileId val can: StateFlow = _can val pin: StateFlow = _pin - val altPairing: StateFlow = _altPairing + val saveCredentials: StateFlow = _saveCredentials + + val authorizationWithExternalAppInBackgroundEvent = ComposableEvent() + val redirectUriEvent = ComposableEvent>() + val redirectUriGematikErrorEvent = ComposableEvent() + val redirectUriErrorEvent = ComposableEvent() init { reset() } + override fun onCleared() { + super.onCleared() + reset() + } + fun reset() { controllerScope.launch { _profileId.value = "" _can.value = "" _pin.value = "" - _altPairing.value = null + _saveCredentials.value = null + } + } + + fun resetPin() { + controllerScope.launch { + _pin.value = "" } } @@ -63,9 +90,48 @@ class CardWallGraphController : SharedController() { _pin.value = value } } - fun setAltPairing(value: AltPairingProvider.AuthResult?) { + fun setSaveCredentials(value: SaveCredentialsController.AuthResult?) { + controllerScope.launch { + _saveCredentials.value = value + } + } + + fun switchToPKV(profileId: ProfileIdentifier) { + controllerScope.launch { + switchProfileToPKVUseCase.invoke(profileId) + } + } + + fun startAuthorizationWithExternal( + gidEventData: GidEventData + ) { controllerScope.launch { - _altPairing.value = value + authorizationWithExternalAppInBackgroundEvent.trigger(true) + @Requirement( + "O.Auth_4#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Start the GID login process with the health insurance company." + ) + getUniversalLinkUseCase.invoke( + universalLinkIdp = UniversalLinkIdp( + authenticatorName = gidEventData.authenticatorName, + authenticatorId = gidEventData.authenticatorId, + profileId = gidEventData.profileId, + isGid = true + ) + ).fold( + onSuccess = { redirectUri -> + authorizationWithExternalAppInBackgroundEvent.trigger(false) + redirectUriEvent.trigger(redirectUri to gidEventData) + }, + onFailure = { + authorizationWithExternalAppInBackgroundEvent.trigger(false) + when (it) { + is GematikResponseError -> redirectUriGematikErrorEvent.trigger(it) + else -> redirectUriErrorEvent.trigger(it.message) + } + } + ) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallNfcPositionState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallNfcPositionState.kt index a9121141..ced1a679 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallNfcPositionState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/CardWallNfcPositionState.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.presentation @@ -31,8 +31,8 @@ class CardWallNfcPositionState( private val findNfc = nfcPositionUseCase.findNfcPositionForPhone() val state = findNfc?.let { - CardWallNfcPositionStateData.State(findNfc) - } ?: CardWallNfcPositionStateData.defaultState + NfcPositionStateData.State(findNfc) + } ?: NfcPositionStateData.defaultState } @Composable @@ -44,7 +44,7 @@ fun rememberCardWallNfcPositionState(): CardWallNfcPositionState { } } -object CardWallNfcPositionStateData { +object NfcPositionStateData { @Immutable data class State( val nfcData: NfcPositionUseCaseData.NfcData diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/ExternalAuthenticatorListController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/ExternalAuthenticatorListController.kt index c5602665..7e2b63d9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/ExternalAuthenticatorListController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/ExternalAuthenticatorListController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.presentation @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.cardwall.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.idp.model.HealthInsuranceData import de.gematik.ti.erp.app.idp.model.UniversalLinkIdp.Companion.toUniversalLinkIdp import de.gematik.ti.erp.app.idp.model.error.GematikResponseError @@ -79,6 +80,11 @@ class DefaultExternalAuthenticatorListController( override val redirectUriErrorEvent = ComposableEvent() + @Requirement( + "O.Auth_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Business logic to get the list of health insurance companies." + ) override fun getHealthInsuranceAppList() { healthInsuranceDataList.value = UiState.Loading() scope.launch { @@ -102,7 +108,11 @@ class DefaultExternalAuthenticatorListController( ) { scope.launch { authorizationWithExternalAppInBackgroundEvent.trigger(true) - + @Requirement( + "O.Auth_4#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Start the GID login process with the health insurance company." + ) getUniversalLinkUseCase.invoke( universalLinkIdp = healthInsuranceData.toUniversalLinkIdp(profileId) ).fold( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/SaveCredentialsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/SaveCredentialsController.kt new file mode 100644 index 00000000..d117b295 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/presentation/SaveCredentialsController.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.presentation + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.secureRandomInstance +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import io.github.aakira.napier.Napier +import kotlinx.coroutines.suspendCancellableCoroutine +import org.bouncycastle.util.encoders.Base64 +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.PublicKey +import java.security.spec.ECGenParameterSpec +import kotlin.coroutines.resume + +private const val KeyStoreAliasKeySize = 32 // in bytes +private const val KeyTimeout = 15 * 60 // in seconds + +@Stable +class SaveCredentialsController( + private val biometricPromptBuilder: BiometricPromptBuilder, + private val promptInfo: BiometricPrompt.PromptInfo +) { + sealed interface AuthResult { + data object Error : AuthResult + data object Authenticated : AuthResult + + @Stable + class Initialized(val aliasOfSecureElementEntry: ByteArray, val publicKey: PublicKey) : AuthResult + } + + @Requirement( + "A_21585#1", + "A_21590#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Initialize biometric authentication for strongbox backed devices." + ) + @RequiresApi(Build.VERSION_CODES.P) + suspend fun initializeAndPrompt(useStrongBox: Boolean): AuthResult = suspendCancellableCoroutine { continuation -> + val aliasOfSecureElementEntry = ByteArray(KeyStoreAliasKeySize).apply { + secureRandomInstance().nextBytes(this) + } + + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + "AndroidKeyStore" + ) + + @Requirement( + "O.Auth_5#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Invalidate when change is registered.", + codeLines = 10 + ) + val parameterSpec = KeyGenParameterSpec.Builder( + Base64.toBase64String(aliasOfSecureElementEntry), + KeyProperties.PURPOSE_SIGN + ).apply { + setInvalidatedByBiometricEnrollment(true) + setUserAuthenticationRequired(true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // While the documentation of Android suggests to set this to zero, this is safe to use. + // If the key is used and later, e.g. a fingerprint is added, the keystore implementation of + // Android will throw a `KeyPermanentlyInvalidatedException`. Later on if the user restarts + // the phone, the key is permanently invalidated and the actual `UserNotAuthenticatedException` + // is thrown. + setUserAuthenticationParameters( + KeyTimeout, + KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG + ) + } else { + // needed for Huawei and Android devices < R + setUserAuthenticationValidityDurationSeconds(KeyTimeout) + } + Napier.d("2 StrongBox is available: $useStrongBox") + setIsStrongBoxBacked(useStrongBox) + setDigests(KeyProperties.DIGEST_SHA256) + setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + }.build() + + keyPairGenerator.initialize(parameterSpec) + val keyPair = keyPairGenerator.generateKeyPair() + val publicKey = keyPair.public // required to init + val prompt = biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + continuation.resume( + AuthResult.Initialized( + aliasOfSecureElementEntry = aliasOfSecureElementEntry, + publicKey = publicKey + ) + ) + }, + onError = { _: String, _: Int -> + cleanup(aliasOfSecureElementEntry) + continuation.resume(AuthResult.Error) + } + ) + prompt.authenticate(promptInfo) + continuation.invokeOnCancellation { + prompt.cancelAuthentication() + cleanup(aliasOfSecureElementEntry) + } + } + + fun cleanup(aliasOfSecureElementEntry: ByteArray) { + try { + KeyStore.getInstance("AndroidKeyStore") + .apply { load(null) } + .deleteEntry(Base64.toBase64String(aliasOfSecureElementEntry)) + } catch (e: KeyStoreException) { + Napier.e("Couldn't remove key from keystore on failure; expected to happen.", e) + } + } +} + +@Composable +fun rememberSaveCredentialsScreenController(): SaveCredentialsController { + val activity = LocalActivity.current as AppCompatActivity + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity) } + val biometricPromptInfo = biometricPromptBuilder.buildPromptInfoWithBestSecureOption( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info), + negativeButton = stringResource(R.string.auth_prompt_cancel) + ) + return remember { + SaveCredentialsController(biometricPromptBuilder, biometricPromptInfo) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallCanScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallCanScreen.kt deleted file mode 100644 index b20fe7b3..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallCanScreen.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource - -const val CAN_LENGTH = 6 - -@Requirement( - "O.Purp_2#2", - "O.Data_6#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "CAN is used for eGK connection." -) -class CardWallCanScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val can by graphController.can.collectAsStateWithLifecycle() - val lazyListState = rememberLazyListState() - CardWallScaffold( - modifier = Modifier.testTag(TestTag.CardWall.CAN.CANScreen), - onBack = { - navController.popBackStack() - }, - title = stringResource(R.string.cdw_top_bar_title), - nextEnabled = can.length == CAN_LENGTH, - onNext = { - navController.navigate(CardWallRoutes.CardWallPinScreen.path()) - }, - listState = lazyListState, - nextText = stringResource(R.string.unlock_egk_next), - actions = { - TextButton( - onClick = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) { innerPadding -> - CanScreenContent( - lazyListState = lazyListState, - innerPadding = innerPadding, - can = can, - onClickLearnMore = { navController.navigate(MainNavigationScreens.OrderHealthCard.path()) }, - onNext = { - navController.navigate(CardWallRoutes.CardWallPinScreen.path()) - }, - onCanChange = { graphController.setCardAccessNumber(it) } - ) - } - } -} - -@Composable -fun CanScreenContent( - lazyListState: LazyListState, - innerPadding: PaddingValues, - onClickLearnMore: () -> Unit, - onNext: () -> Unit, - can: String, - onCanChange: (String) -> Unit -) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } - LazyColumn( - state = lazyListState, - modifier = Modifier.fillMaxSize(), - contentPadding = contentPadding - ) { - item { - HealthCardCanImage() - } - item { - CanDescription(onClickLearnMore) - SpacerXXLarge() - } - item { - CanInputField( - modifier = Modifier.scrollOnFocus(to = 2, lazyListState), - can = can, - onCanChange = onCanChange, - next = onNext - ) - } - } -} - -@Composable -fun HealthCardCanImage() { - Column(modifier = Modifier.wrapContentHeight()) { - Image( - painterResource(R.drawable.card_wall_card_can), - null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth() - ) - SpacerXXLarge() - } -} - -@Composable -fun CanDescription(onClickLearnMore: () -> Unit) { - Column { - Text( - stringResource(R.string.cdw_can_headline), - style = AppTheme.typography.h5 - ) - SpacerSmall() - Text( - stringResource(R.string.cdw_can_description), - style = AppTheme.typography.body1 - ) - SpacerSmall() - ClickableTaggedText( - text = annotatedLinkStringLight( - uri = "", - text = stringResource(R.string.cdw_no_can_on_card) - ), - onClick = { onClickLearnMore() }, - style = AppTheme.typography.body2, - modifier = Modifier.align(Alignment.End) - .testTag(TestTag.CardWall.CAN.OrderEgkButton) - ) - } -} - -@Composable -fun CanInputField( - modifier: Modifier, - can: String, - onCanChange: (String) -> Unit, - next: () -> Unit -) { - val canRegex = """^\d{0,$CAN_LENGTH}$""".toRegex() - OutlinedTextField( - modifier = modifier - .testTag(TestTag.CardWall.CAN.CANField) - .fillMaxWidth(), - value = can, - onValueChange = { - if (it.matches(canRegex)) { - onCanChange(it) - } - }, - label = { Text(stringResource(R.string.can_input_field_label)) }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.NumberPassword, - imeAction = ImeAction.Next - ), - shape = RoundedCornerShape(SizeDefaults.one), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), - keyboardActions = KeyboardActions { - if (can.length == CAN_LENGTH) { - next() - } - } - ) - SpacerTiny() - Text( - text = annotatedStringResource( - R.string.cdw_can_length_info, - CAN_LENGTH.toString() - ), - style = AppTheme.typography.caption1l - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallExternalAuthenticationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallExternalAuthenticationScreen.kt deleted file mode 100644 index c214e559..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallExternalAuthenticationScreen.kt +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import android.app.Dialog -import android.os.Build -import android.provider.Settings -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.base.openSettingsAsNewActivity -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.presentation.rememberExternalAuthenticatorListController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallExternalAuthenticationScreenBannerSection -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallExternalAuthenticationScreenHeaderSection -import de.gematik.ti.erp.app.cardwall.ui.components.GematikErrorDialog -import de.gematik.ti.erp.app.core.LocalIntentHandler -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.idp.model.HealthInsuranceData -import de.gematik.ti.erp.app.idp.model.HealthInsuranceData.Companion.isPkv -import de.gematik.ti.erp.app.mainscreen.ui.LoadingDialog -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ComposableEvent -import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger -import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.UiStateMachine -import de.gematik.ti.erp.app.utils.extensions.LocalDialog -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar -import de.gematik.ti.erp.app.utils.uistate.UiState -import kotlinx.coroutines.android.awaitFrame -import kotlinx.coroutines.launch - -class CardWallExternalAuthenticationScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val context = LocalContext.current - val profileId by graphController.profileId.collectAsStateWithLifecycle() - val intentHandler = LocalIntentHandler.current - val dialog = LocalDialog.current - var loadingDialog: Dialog? = remember { null } - - val listState = rememberLazyListState() - - val controller = rememberExternalAuthenticatorListController() - val healthInsuranceAppIdps by controller.healthInsuranceDataList.collectAsStateWithLifecycle() - val authorizationWithExternalAppEvent = controller.authorizationWithExternalAppInBackgroundEvent - val redirectUriEvent = controller.redirectUriEvent - val redirectErrorEvent = controller.redirectUriErrorEvent - val redirectGematikErrorEvent = controller.redirectUriGematikErrorEvent - - LaunchedEffect(Unit) { - controller.getHealthInsuranceAppList() - } - - authorizationWithExternalAppEvent.listen { isStarted -> - if (isStarted) { - dialog.show { - loadingDialog = it - LoadingDialog { it.dismiss() } - } - } else { - loadingDialog?.dismiss() - } - } - - redirectUriEvent.listen { (redirectUri, healthInsuranceData) -> - intentHandler.tryStartingExternalHealthInsuranceAuthenticationApp( - redirect = redirectUri, - onSuccess = { - if (healthInsuranceData.isPkv()) { - controller.switchToPKV(profileId) - } - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - }, - onFailure = { - dialog.show { - ErezeptAlertDialog( - title = stringResource(R.string.gid_external_app_missing_title), - body = stringResource(R.string.gid_external_app_missing_description), - okText = stringResource(R.string.ok), - onConfirmRequest = { it.dismiss() }, - onDismissRequest = { it.dismiss() } - ) - } - } - ) - } - - redirectErrorEvent.listen { - dialog.show { - ErezeptAlertDialog( - title = stringResource(R.string.main_fasttrack_error_title), - body = stringResource(R.string.main_fasttrack_error_info), - okText = stringResource(R.string.ok), - onConfirmRequest = { it.dismiss() }, - onDismissRequest = { it.dismiss() } - ) - } - } - - redirectGematikErrorEvent.listen { responseError -> - dialog.show { - GematikErrorDialog(error = responseError) { - it.dismiss() - } - } - } - ExternalAuthenticationListScreenScaffold( - profileId = profileId, - listState = listState, - healthInsuranceAppIdps = healthInsuranceAppIdps, - filterList = controller::filterList, - unFilterList = controller::unFilterList, - reloadHealthInsuranceAppList = controller::getHealthInsuranceAppList, - startAuthorizationWithExternal = controller::startAuthorizationWithExternal, - onClickBanner = { - context.openSettingsAsNewActivity( - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> - Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS - - else -> Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS - } - ) - }, - onCancel = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - }, - onBack = navController::popBackStack - - ) - } -} - -@Composable -private fun ExternalAuthenticationListScreenScaffold( - profileId: ProfileIdentifier, - listState: LazyListState, - healthInsuranceAppIdps: UiState>, - filterList: (String) -> Unit, - unFilterList: () -> Unit, - reloadHealthInsuranceAppList: () -> Unit, - startAuthorizationWithExternal: (ProfileIdentifier, HealthInsuranceData) -> Unit, - onClickBanner: () -> Unit, - onCancel: () -> Unit, - onBack: () -> Unit -) { - AnimatedElevationScaffold( - navigationMode = NavigationBarMode.Back, - topBarTitle = stringResource(R.string.cdw_fasttrack_title), - onBack = onBack, - listState = listState, - actions = { - TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) - } - } - ) { - ExternalAuthenticationScreenContent( - profileId = profileId, - listState = listState, - healthInsuranceAppIdps = healthInsuranceAppIdps, - onClickBanner = onClickBanner, - onSearch = { searchWord -> - if (searchWord.isNotEmpty()) { - filterList(searchWord) - } else { - unFilterList() - } - }, - onClickRetry = { - reloadHealthInsuranceAppList() - }, - onClickHealthInsuranceIdp = { profileId, heathInsuranceIdp -> - startAuthorizationWithExternal( - profileId, - heathInsuranceIdp - ) - } - ) - } -} - -@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) -@Composable -fun ExternalAuthenticationScreenContent( - profileId: ProfileIdentifier, - listState: LazyListState, - onSearch: (String) -> Unit, - healthInsuranceAppIdps: UiState>, - onClickHealthInsuranceIdp: (ProfileIdentifier, HealthInsuranceData) -> Unit, - onClickBanner: () -> Unit, - onClickRetry: () -> Unit -) { - val fastTrackClosedString = stringResource(R.string.gid_fast_track_closed_error) - - // focus maintenance - val contentFocusRequester = remember { FocusRequester() } - val emptyFocusRequester = remember { FocusRequester() } - val keyboard = LocalSoftwareKeyboardController.current - val coroutineScope = rememberCoroutineScope() - val focusContentEvent = ComposableEvent() - val focusEmptyEvent = ComposableEvent() - - var showBanner by remember { mutableStateOf(true) } - var search by remember { mutableStateOf(TextFieldValue("")) } - - val snackbar = LocalSnackbar.current - - focusContentEvent.listen { - coroutineScope.launch { - contentFocusRequester.requestFocus() - awaitFrame() - keyboard?.show() - } - } - - focusEmptyEvent.listen { - coroutineScope.launch { - emptyFocusRequester.requestFocus() - awaitFrame() - keyboard?.show() - } - } - - UiStateMachine( - state = healthInsuranceAppIdps, - onLoading = { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium) - ) { - item { - Box( - modifier = Modifier - .fillParentMaxSize() - .padding(PaddingDefaults.Large), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier - .size(PaddingDefaults.XLarge) - .align(Alignment.Center) - ) - } - } - } - }, - onError = { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium), - state = listState - ) { - item { - ErrorScreen( - modifier = Modifier.fillParentMaxSize(), - onClickRetry = onClickRetry - ) - } - } - }, - onEmpty = { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium), - state = listState - ) { - focusEmptyEvent.trigger() - CardWallExternalAuthenticationScreenBannerSection( - showBanner = showBanner, - onClickBanner = onClickBanner, - onClose = { showBanner = false } - ) - CardWallExternalAuthenticationScreenHeaderSection( - modifier = Modifier.fillMaxWidth(), - searchValue = search, - focusRequester = emptyFocusRequester, - onValueChange = { - onSearch(it.text) - search = it.copy(selection = TextRange(it.text.length)) - } - ) - } - }, - onContent = { healthInsuranceAppIdpValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - focusContentEvent.trigger() - CardWallExternalAuthenticationScreenBannerSection( - showBanner = showBanner, - onClickBanner = onClickBanner, - onClose = { showBanner = false } - ) - CardWallExternalAuthenticationScreenHeaderSection( - modifier = Modifier.fillMaxWidth(), - searchValue = search, - focusRequester = contentFocusRequester, - onValueChange = { - onSearch(it.text) - search = it.copy(selection = TextRange(it.text.length)) - } - ) - items(healthInsuranceAppIdpValues) { - Surface( - modifier = Modifier.fillMaxWidth(), - onClick = { - if (it.isGid) { - onClickHealthInsuranceIdp(profileId, it) - } else { - snackbar.show(fastTrackClosedString) - } - } - ) { - Text(text = it.name) - } - } - } - } - ) -} - -@Composable -private fun ErrorScreen( - modifier: Modifier = Modifier, - onClickRetry: () -> Unit -) = - Box( - modifier = modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.align(BiasAlignment(horizontalBias = 0f, verticalBias = -0.33f)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - Text( - stringResource(R.string.cdw_fasttrack_error_title), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - Text( - stringResource(R.string.cdw_fasttrack_error_info), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - TextButton( - onClick = onClickRetry - ) { - Icon(Icons.Rounded.Refresh, null) - SpacerSmall() - Text(stringResource(R.string.cdw_fasttrack_try_again)) - } - } - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallIntroScreen.kt deleted file mode 100644 index 4fb91786..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallIntroScreen.kt +++ /dev/null @@ -1,419 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.base.openSettingsAsNewActivity -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.cardwall.ui.components.EnableNfcDialog -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.demomode.DemoModeIntent -import de.gematik.ti.erp.app.demomode.startAppWithDemoMode -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.gid.ui.DomainsNotVerifiedDialog -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.ClickText -import de.gematik.ti.erp.app.utils.compose.ClickableText -import de.gematik.ti.erp.app.utils.compose.ComposableEvent -import de.gematik.ti.erp.app.utils.compose.HintTextActionButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXLarge -import de.gematik.ti.erp.app.utils.extensions.LocalDialog - -@Requirement( - "O.Auth_3#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Selection of Authentication with health card or insurance App" -) -class CardWallIntroScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val profileId = remember { navBackStackEntry.arguments?.getString(CardWallRoutes.profileId) ?: "" } - graphController.setProfileId(profileId) - - val lazyListState = rememberLazyListState() - val cardWallController = rememberCardWallController() - - val activity = LocalActivity.current - val dialog = LocalDialog.current - val context = LocalContext.current - - val nfcDisabledEvent = ComposableEvent() - val domainsAreNotVerifiedEvent = ComposableEvent() - - val isDomainVerified = cardWallController.isDomainVerified - val isNfcAvailable by cardWallController.isNFCAvailable - - CardWallScaffold( - modifier = Modifier.testTag(TestTag.CardWall.Intro.IntroScreen) - .systemBarsPadding(), - title = "", - listState = lazyListState, - backMode = null, - actions = { - TextButton( - onClick = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - } - ) { - Text(stringResource(R.string.cancel)) - } - }, - onNext = null, - nextText = "", - onBack = { navController.popBackStack() } - ) { - CardWallIntroScreenContent( - isNfcAvailable = isNfcAvailable, - isDomainVerified = isDomainVerified, - listState = lazyListState, - checkNfcEnabled = { cardWallController.checkNfcEnabled() }, - showEnableNFCDialog = { nfcDisabledEvent.trigger(Unit) }, - onClickOrderNow = { navController.navigate(MainNavigationScreens.OrderHealthCard.path()) }, - onClickHealthCardAuth = { navController.navigate(CardWallRoutes.CardWallCanScreen.path()) }, - onClickInsuranceAuth = { - navController.navigate(CardWallRoutes.CardWallExternalAuthenticationScreen.path()) - }, - showVerifyDomainDialog = { domainsAreNotVerifiedEvent.trigger(Unit) }, - onClickDemoMode = { - DemoModeIntent.startAppWithDemoMode(activity = activity) - } - ) - } - nfcDisabledEvent.listen { - dialog.show { - EnableNfcDialog( - onClickAction = { it.dismiss() }, - onCancel = { it.dismiss() } - ) - } - } - domainsAreNotVerifiedEvent.listen { - dialog.show { - DomainsNotVerifiedDialog( - onClickSettingsOpen = { - context.openSettingsAsNewActivity() - it.dismiss() - }, - onDismissRequest = { it.dismiss() } - ) - } - } - } -} - -@Composable -private fun CardWallIntroScreenContent( - isNfcAvailable: Boolean, - isDomainVerified: Boolean, - listState: LazyListState, - checkNfcEnabled: () -> Boolean, - showEnableNFCDialog: () -> Unit, - onClickHealthCardAuth: () -> Unit, - onClickInsuranceAuth: () -> Unit, - onClickOrderNow: () -> Unit, - showVerifyDomainDialog: () -> Unit, - onClickDemoMode: () -> Unit -) { - LazyColumn( - state = listState, - modifier = Modifier.padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - HealthCardPhoneImage() - Header() - SubTitleHeader() - HealthCardLoginSection( - isNfcAvailable, - checkNfcEnabled, - onClickHealthCardAuth, - showEnableNFCDialog - ) - GidLoginSection( - isDomainVerified = isDomainVerified, - showVerifyDomainDialog = showVerifyDomainDialog, - onClick = onClickInsuranceAuth - ) - OrderHealthCardHintSection(onClickOrderNow) - TryDemomodeSection(onClickDemoMode) - } -} - -@Suppress("FunctionName") -private fun LazyListScope.HealthCardPhoneImage() { - item { - Image( - painterResource(R.drawable.card_wall_card_hand), - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxWidth().wrapContentHeight() - ) - } -} - -@Suppress("FunctionName") -private fun LazyListScope.Header() { - item { - Text( - stringResource(R.string.cdw_intro_header), - style = AppTheme.typography.h5, - color = AppTheme.colors.neutral900, - modifier = Modifier.testTag("cdw_txt_intro_header_bottom") - ) - } -} - -@Suppress("FunctionName") -private fun LazyListScope.SubTitleHeader() { - item { SpacerSmall() } - item { - Text( - stringResource(R.string.cdw_intro_info), - style = AppTheme.typography.subtitle2, - textAlign = TextAlign.Center, - color = AppTheme.colors.neutral600 - ) - } - item { SpacerXLarge() } -} - -@Suppress("FunctionName") -@OptIn(ExperimentalMaterialApi::class) -private fun LazyListScope.HealthCardLoginSection( - isNfcAvailable: Boolean, - checkNfcEnabled: () -> Boolean, - onClick: () -> Unit, - showEnableNFCDialog: () -> Unit -) { - item { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - stringResource(R.string.cdw_intro_auth_prior), - modifier = Modifier.align(Alignment.Start).offset(x = PaddingDefaults.Medium), - style = AppTheme.typography.subtitle2, - fontWeight = FontWeight.Bold, - color = AppTheme.colors.primary600 - ) - Card( - modifier = CardPaddingModifier, - shape = RoundedCornerShape(SizeDefaults.double), - border = BorderStroke(SizeDefaults.quarter, color = AppTheme.colors.primary600), - elevation = SizeDefaults.zero, - backgroundColor = AppTheme.colors.neutral050, - enabled = isNfcAvailable, - onClick = { - if (checkNfcEnabled()) { - onClick() - } else { - showEnableNFCDialog() - } - } - ) { - Row( - Modifier.padding(PaddingDefaults.Medium) - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - stringResource(R.string.cdw_intro_auth_health_card), - style = AppTheme.typography.subtitle1l, - color = when { - isNfcAvailable -> AppTheme.colors.neutral900 - else -> AppTheme.colors.neutral400 - } - ) - SpacerTiny() - Text( - when { - isNfcAvailable -> stringResource(R.string.cdw_intro_auth_health_card_pin) - else -> stringResource(R.string.cdw_intro_auth_health_card_no_nfc_device) - }, - style = AppTheme.typography.body2l, - color = when { - isNfcAvailable -> AppTheme.colors.neutral600 - else -> AppTheme.colors.neutral400 - } - ) - } - Icon( - Icons.Filled.KeyboardArrowRight, - null, - tint = if (isNfcAvailable) AppTheme.colors.primary600 else AppTheme.colors.neutral300, - modifier = Modifier - .size(SizeDefaults.triple) - .align(Alignment.CenterVertically) - ) - } - } - } - } -} - -@Suppress("FunctionName") -@OptIn(ExperimentalMaterialApi::class) -private fun LazyListScope.GidLoginSection( - isDomainVerified: Boolean, - showVerifyDomainDialog: () -> Unit, - onClick: () -> Unit -) { - item { - Card( - modifier = CardPaddingModifier, - shape = RoundedCornerShape(SizeDefaults.double), - border = BorderStroke(SizeDefaults.eighth, color = AppTheme.colors.neutral300), - elevation = SizeDefaults.zero, - backgroundColor = AppTheme.colors.neutral050, - onClick = { - when { - isDomainVerified -> onClick() - else -> showVerifyDomainDialog() - } - } - ) { - Row( - modifier = Modifier.padding( - PaddingDefaults.Medium - ), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier - .weight(1f) - ) { - Text( - stringResource(R.string.cdw_intro_auth_gid), - style = AppTheme.typography.subtitle1l, - color = AppTheme.colors.neutral900 - ) - SpacerTiny() - Text( - stringResource(R.string.cdw_intro_auth_additional_app_required), - style = AppTheme.typography.body2l, - color = AppTheme.colors.neutral600 - ) - } - Icon( - Icons.Filled.KeyboardArrowRight, - null, - tint = AppTheme.colors.neutral400, - modifier = Modifier - .size(SizeDefaults.triple) - .align(Alignment.CenterVertically) - ) - } - } - } -} - -@Suppress("FunctionName") -private fun LazyListScope.OrderHealthCardHintSection(onClickOrderNow: () -> Unit) { - item { SpacerSmall() } - item { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - text = stringResource(R.string.cdw_have_no_card_with_pin), - style = AppTheme.typography.body2l - ) - HintTextActionButton( - text = stringResource(R.string.cdw_intro_order_now), - align = Alignment.End, - modifier = Modifier - .align(Alignment.End) - .testTag(TestTag.CardWall.Intro.OrderEgkButton) - ) { - onClickOrderNow() - } - } - } -} - -@Suppress("FunctionName") -private fun LazyListScope.TryDemomodeSection(onClickDemoMode: () -> Unit) { - item { SpacerMedium() } - item { - ClickableText( - modifier = Modifier.padding(horizontal = PaddingDefaults.Large), - textWithPlaceholdersRes = R.string.demo_mode_start_text, - textStyle = AppTheme.typography.body1l, - clickText = ClickText( - text = stringResource(R.string.demo_mode_link_text), - onClick = onClickDemoMode - ) - ) - } -} - -private val CardPaddingModifier = Modifier - .padding( - bottom = PaddingDefaults.Medium - ) - .fillMaxWidth() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallPinScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallPinScreen.kt deleted file mode 100644 index d2c82cae..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallPinScreen.kt +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.domain.biometric.deviceStrongBiometricStatus -import de.gematik.ti.erp.app.cardwall.domain.biometric.hasDeviceStrongBox -import de.gematik.ti.erp.app.cardwall.domain.biometric.isDeviceSupportsBiometric -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import de.gematik.ti.erp.app.utils.compose.visualTestTag - -val PIN_RANGE = 6..8 - -@Requirement( - "O.Purp_2#3", - "O.Data_6#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "PIN is used for eGK connection." -) -class CardWallPinScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val pin by graphController.pin.collectAsStateWithLifecycle() - val lazyListState = rememberLazyListState() - val context = LocalContext.current - val biometricMode = remember { deviceStrongBiometricStatus(context) } - val onNext = { - val deviceSupportsBiometric = isDeviceSupportsBiometric(biometricMode) - - @Requirement( - "O.Biom_2#1", - "O.Biom_3#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Only if device has strongbox, the user can select authentication with " + - "biometric prompt" - ) - val deviceSupportsStrongbox = hasDeviceStrongBox(context) - if (deviceSupportsBiometric && - deviceSupportsStrongbox - ) { - navController.navigate(CardWallRoutes.CardWallSaveCredentialsScreen.path()) - } else { - navController.navigate(CardWallRoutes.CardWallReadCardScreen.path()) - } - } - CardWallScaffold( - modifier = Modifier.testTag(TestTag.CardWall.PIN.PinScreen), - onBack = { - navController.popBackStack() - }, - title = stringResource(R.string.cdw_top_bar_title), - nextEnabled = pin.length in PIN_RANGE, - onNext = onNext, - listState = lazyListState, - nextText = stringResource(R.string.unlock_egk_next), - actions = { - TextButton( - onClick = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) { innerPadding -> - CardWallPinScreenContent( - lazyListState = lazyListState, - innerPadding = innerPadding, - pin = pin, - onNext = onNext, - onPinChange = { graphController.setPersonalIdentificationNumber(it) }, - onClickNoPinReceived = { - navController.navigate(MainNavigationScreens.OrderHealthCard.path()) - } - ) - } - } -} - -@Composable -private fun CardWallPinScreenContent( - lazyListState: LazyListState, - innerPadding: PaddingValues, - onNext: () -> Unit, - pin: String, - onPinChange: (String) -> Unit, - onClickNoPinReceived: () -> Unit -) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } - LazyColumn( - state = lazyListState, - modifier = Modifier.fillMaxSize(), - contentPadding = contentPadding - ) { - item { - Text( - stringResource(R.string.cdw_pin_title), - style = AppTheme.typography.h5 - ) - SpacerSmall() - } - item { - Text( - stringResource(R.string.cdw_pin_info), - style = AppTheme.typography.body1 - ) - SpacerSmall() - } - item { - ClickableTaggedText( - modifier = Modifier.testTag(TestTag.CardWall.PIN.OrderEgkButton), - text = annotatedLinkStringLight( - uri = "", - text = stringResource(R.string.cdw_no_pin_received) - ), - onClick = { onClickNoPinReceived() }, - style = AppTheme.typography.body2 - ) - SpacerXXLarge() - } - item { - PinInputField( - modifier = Modifier - .fillMaxWidth() - .scrollOnFocus(to = 3, lazyListState), - pinRange = PIN_RANGE, - onPinChange = onPinChange, - pin = pin, - onNext = onNext, - infoText = annotatedStringResource( - R.string.cdw_pin_length_info, - PIN_RANGE.first.toString(), - PIN_RANGE.last.toString() - ).text - ) - } - } -} - -@Requirement( - "O.Data_10", - "O.Data_11", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Password fields using the keyboard type numberPassword. Copying the content is not possible " + - "with this type. Autocorrect is disallowed. It`s not possible to disable third party keyboards." -) -@Composable -fun PinInputField( - modifier: Modifier, - pinRange: IntRange, - onPinChange: (String) -> Unit, - pin: String, - label: String = stringResource(R.string.cdw_pin_label), - isConsistent: Boolean = true, - infoText: String, - onNext: () -> Unit -) { - val secretRegexString = "^\\d{0,${pinRange.last}}$" - val secretRegex = secretRegexString.toRegex() - var secretVisible by remember { mutableStateOf(false) } - OutlinedTextField( - modifier = modifier.visualTestTag(TestTag.CardWall.PIN.PINField), - value = pin, - onValueChange = { - if (it.matches(secretRegex)) { - onPinChange(it) - } - }, - label = { Text(label) }, - visualTransformation = if (secretVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.NumberPassword, - imeAction = ImeAction.Next - ), - shape = RoundedCornerShape(SizeDefaults.one), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), - keyboardActions = KeyboardActions { - if (isConsistent && pin.length in pinRange) { - onNext() - } - }, - trailingIcon = { - IconToggleButton( - checked = secretVisible, - onCheckedChange = { secretVisible = it } - ) { - Icon( - if (secretVisible) { - Icons.Rounded.Visibility - } else { - Icons.Rounded.VisibilityOff - }, - null - ) - } - } - ) - SpacerTiny() - Text( - text = infoText, - style = AppTheme.typography.caption1l - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallReadCardScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallReadCardScreen.kt deleted file mode 100644 index e0393106..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallReadCardScreen.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.navOptions -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.AltPairingProvider -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallAuthenticationData -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallAuthenticationDialog -import de.gematik.ti.erp.app.cardwall.ui.components.EnableNfcDialog -import de.gematik.ti.erp.app.cardwall.ui.components.ReadCardScreenComposable -import de.gematik.ti.erp.app.cardwall.ui.components.rememberCardWallAuthenticationDialogState -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes - -class CardWallReadCardScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val cardWallController = rememberCardWallController() - val dialogState = rememberCardWallAuthenticationDialogState() - - val profileId by graphController.profileId.collectAsStateWithLifecycle() - val altPairing by graphController.altPairing.collectAsStateWithLifecycle() - val can by graphController.can.collectAsStateWithLifecycle() - val pin by graphController.pin.collectAsStateWithLifecycle() - - val authenticationData by remember(altPairing) { - derivedStateOf { - (altPairing as? AltPairingProvider.AuthResult.Initialized)?.let { - CardWallAuthenticationData.AltPairingWithHealthCard( - cardAccessNumber = can, - personalIdentificationNumber = pin, - initialPairingData = it - ) - } ?: CardWallAuthenticationData.HealthCard( - cardAccessNumber = can, - personalIdentificationNumber = pin - ) - } - } - - ReadCardScreenComposable( - onBack = { - navController.popBackStack() - }, - onClickTroubleshooting = { - navController.navigate( - TroubleShootingRoutes.TroubleShootingIntroScreen.path() - ) - } - ) - if (!cardWallController.checkNfcEnabled()) { - EnableNfcDialog { - navController.popBackStack() - } - } else { - CardWallAuthenticationDialog( - dialogState = dialogState, - cardWallController = cardWallController, - authenticationData = authenticationData, - profileId = profileId, - troubleShootingEnabled = true, - allowUserCancellation = true, - onFinal = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - }, - onUnlockEgk = { - graphController.reset() - navController.navigate( - CardUnlockRoutes.CardUnlockIntroScreen.path(unlockMethod = UnlockMethod.ResetRetryCounter.name), - navOptions = navOptions { - popUpTo(CardWallRoutes.CardWallIntroScreen.route) { - inclusive = true - } - } - ) - }, - onRetryCan = { - navController.navigate( - CardWallRoutes.CardWallCanScreen.path(), - navOptions = navOptions { - popUpTo(CardWallRoutes.CardWallCanScreen.route) { - inclusive = true - } - } - ) - }, - onRetryPin = { - navController.navigate( - CardWallRoutes.CardWallPinScreen.path(), - navOptions = navOptions { - popUpTo(CardWallRoutes.CardWallPinScreen.route) { - inclusive = true - } - } - ) - }, - onClickTroubleshooting = { - navController.navigate( - TroubleShootingRoutes.TroubleShootingIntroScreen.path() - ) - } - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsInfoScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsInfoScreen.kt deleted file mode 100644 index da32c224..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsInfoScreen.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import android.os.Build -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.navigation.navOptions -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.AltPairingProvider -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.presentation.rememberAltPairingProvider -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import kotlinx.coroutines.launch - -@Requirement( - "O.Biom_1", - "O.Biom_8", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Authentication via biometrics is only possible in combination with " + - "successful authentication via eGK + PIN." -) -class CardWallSaveCredentialsInfoScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - val altPairingProvider = rememberAltPairingProvider() - val scope = rememberCoroutineScope() - val listState = rememberLazyListState() - val altPairing by graphController.altPairing.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - (altPairing as? AltPairingProvider.AuthResult.Initialized)?.let { - altPairingProvider.cleanup(it.aliasOfSecureElementEntry) - graphController.setAltPairing(null) - } - } - CardWallSaveCredentialsInfoScreenScaffold( - onBack = { - navController.popBackStack() - }, - onAccept = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - scope.launch { - when (val r = altPairingProvider.initializeAndPrompt()) { - is AltPairingProvider.AuthResult.Initialized -> { - graphController.setAltPairing(r) - navController.navigate( - CardWallRoutes.CardWallReadCardScreen.path(), - navOptions = navOptions { - popUpTo(CardWallRoutes.CardWallSaveCredentialsScreen.route) - } - ) - } - else -> { - navController.popBackStack() - } - } - } - } else { - navController.popBackStack() - } - }, - listState = listState - ) { innerPadding -> - CardWallSaveCredentialsInfoScreenContent( - listState = listState, - innerPadding = innerPadding - ) - } - } -} - -@Composable -private fun CardWallSaveCredentialsInfoScreenContent( - listState: LazyListState, - innerPadding: PaddingValues -) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } - LazyColumn( - contentPadding = contentPadding, - state = listState - ) { - item { - Text( - stringResource(R.string.cdw_info_header), - style = AppTheme.typography.h5 - ) - SpacerSmall() - } - item { - Text( - stringResource(R.string.cdw_info_first), - style = AppTheme.typography.body1 - ) - SpacerSmall() - } - item { - Text( - stringResource(R.string.cdw_info_second), - style = AppTheme.typography.body1 - ) - SpacerSmall() - } - item { - Text( - stringResource(R.string.cdw_info_third), - style = AppTheme.typography.body1 - ) - SpacerXXLarge() - } - } -} - -@Composable -fun CardWallSaveCredentialsInfoScreenScaffold( - listState: LazyListState, - onAccept: () -> Unit, - onBack: () -> Unit, - content: @Composable (PaddingValues) -> Unit -) { - AnimatedElevationScaffold( - modifier = Modifier.systemBarsPadding(), - topBarTitle = stringResource(R.string.cdw_info_title), - topBarColor = MaterialTheme.colors.background, - listState = listState, - navigationMode = NavigationBarMode.Close, - bottomBar = { - CardWallSaveCredentialsInfoScreenBottomBar( - onNext = onAccept - ) - }, - onBack = onBack, - content = content - ) -} - -@Composable -private fun CardWallSaveCredentialsInfoScreenBottomBar( - onNext: () -> Unit -) { - Surface( - color = MaterialTheme.colors.surface, - elevation = SizeDefaults.half - ) { - Column( - Modifier - .navigationBarsPadding() - .fillMaxWidth() - ) { - PrimaryButton( - onClick = onNext, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTag.CardWall.SecurityAcceptance.AcceptButton) - .padding( - horizontal = SizeDefaults.ninefold, - vertical = PaddingDefaults.ShortMedium - ) - ) { - Text( - stringResource(R.string.cdw_info_accept) - ) - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsScreen.kt deleted file mode 100644 index 2f06a22f..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSaveCredentialsScreen.kt +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import android.content.Intent -import android.os.Build -import android.provider.Settings -import androidx.biometric.BiometricManager -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Fingerprint -import androidx.compose.material.icons.rounded.RadioButtonUnchecked -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.content.ContextCompat -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.domain.biometric.deviceStrongBiometricStatus -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen -import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController -import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.HintCard -import de.gematik.ti.erp.app.utils.compose.HintSmallImage -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge - -private enum class AuthenticationMethod { - None, - Alternative, // e.g. biometrics - HealthCard -} - -class CardWallSaveCredentialsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: CardWallGraphController -) : CardWallScreen() { - @Composable - override fun Content() { - var selectedAuthMode by remember { mutableStateOf(AuthenticationMethod.None) } - val context = LocalContext.current - val biometricMode by produceState(initialValue = BiometricManager.BIOMETRIC_STATUS_UNKNOWN) { - value = deviceStrongBiometricStatus(context) - } - var showEnrollBiometricDialog by remember { mutableStateOf(false) } - var showFutureLogOutHint by remember { mutableStateOf(false) } - val enrollBiometricIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Intent(Settings.ACTION_BIOMETRIC_ENROLL) - } else { - Intent(Settings.ACTION_APPLICATION_SETTINGS) - } - - val lazyListState = rememberLazyListState() - CardWallScaffold( - modifier = Modifier - .testTag("cardWall/authenticationSelection"), - title = stringResource(R.string.cdw_top_bar_title), - nextEnabled = selectedAuthMode != AuthenticationMethod.None, - onNext = { - navController.navigate( - CardWallRoutes.CardWallReadCardScreen.path() - ) - }, - onBack = { - navController.popBackStack() - }, - listState = lazyListState, - nextText = stringResource(R.string.cdw_next), - actions = { - TextButton(onClick = { - graphController.reset() - navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) - }) { - Text(stringResource(R.string.cancel)) - } - } - ) { innerPadding -> - CardWallSaveCredentialsScreenContent( - listState = lazyListState, - innerPadding = innerPadding, - selectedAuthMode = selectedAuthMode, - biometricMode = biometricMode, - showFutureLogOutHint = showFutureLogOutHint, - onShowBiometricDialog = { showEnrollBiometricDialog = it }, - onShowFutureLogOutHint = { showFutureLogOutHint = it }, - onSelectAlternativeOption = { selectedAuthMode = it }, - onOpenInfoScreen = { - navController.navigate( - CardWallRoutes.CardWallSaveCredentialsInfoScreen.path() - ) - } - ) - } - if (showEnrollBiometricDialog) { - CommonAlertDialog( - icon = Icons.Rounded.Fingerprint, - header = stringResource(R.string.enroll_biometric_dialog_header), - info = stringResource(R.string.enroll_biometric_dialog_info), - cancelText = stringResource(R.string.enroll_biometric_dialog_cancel), - actionText = stringResource(R.string.enroll_biometric_dialog_settings), - onCancel = { showEnrollBiometricDialog = false } - ) { - ContextCompat.startActivity(context, enrollBiometricIntent, null) - } - } - } -} - -@Composable -private fun CardWallSaveCredentialsScreenContent( - listState: LazyListState, - innerPadding: PaddingValues, - selectedAuthMode: AuthenticationMethod, - biometricMode: Int, - showFutureLogOutHint: Boolean, - onSelectAlternativeOption: (AuthenticationMethod) -> Unit, - onShowBiometricDialog: (Boolean) -> Unit, - onShowFutureLogOutHint: (Boolean) -> Unit, - onOpenInfoScreen: () -> Unit -) { - val contentPadding by remember(innerPadding) { - derivedStateOf { - PaddingValues( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - } - LazyColumn( - state = listState, - contentPadding = contentPadding - ) { - item { - Text( - stringResource(R.string.cdw_selection_title), - style = AppTheme.typography.h5, - modifier = Modifier.padding(PaddingDefaults.Medium) - ) - SpacerXXLarge() - } - item { - SelectableCard( - modifier = Modifier.testTag(TestTag.CardWall.StoreCredentials.Save), - selected = selectedAuthMode == AuthenticationMethod.Alternative, - startIcon = Icons.Rounded.Check, - text = stringResource(R.string.cdw_selection_save) - ) { - onShowFutureLogOutHint(false) - if (biometricMode == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { - onShowBiometricDialog(true) - } else { - onSelectAlternativeOption(AuthenticationMethod.Alternative) - onOpenInfoScreen() - } - } - SpacerMedium() - } - item { - SelectableCard( // TODO fix - modifier = Modifier - .testTag(TestTag.CardWall.StoreCredentials.DontSave), - selected = selectedAuthMode == AuthenticationMethod.HealthCard, - startIcon = Icons.Rounded.Close, - text = stringResource(R.string.cdw_selection_save_not) - ) { - onSelectAlternativeOption(AuthenticationMethod.HealthCard) - onShowFutureLogOutHint(true) - } - SpacerXXLarge() - } - item { - if (showFutureLogOutHint) { - SpacerMedium() - HintCard( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - image = { - HintSmallImage(painterResource(R.drawable.information), null, it) - }, - title = { Text(stringResource(R.string.cdw_selection_hint_title)) }, - body = { Text(stringResource(R.string.cdw_selection_hint_info_)) } - ) - SpacerXXLarge() - } - } - } -} - -@Composable -private fun SelectableCard( - modifier: Modifier = Modifier, - selected: Boolean = false, - startIcon: ImageVector, - text: String, - onCardSelected: () -> Unit = {} -) { - val checkIcon = if (selected) { - Icons.Rounded.CheckCircle - } else { - Icons.Rounded.RadioButtonUnchecked - } - - val checkIconTint = if (selected) { - AppTheme.colors.primary600 - } else { - AppTheme.colors.neutral400 - } - - val cardBorderStroke = if (selected) { - BorderStroke(SizeDefaults.quarter, AppTheme.colors.primary600) - } else { - BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral300) - } - - Card( - border = cardBorderStroke, - backgroundColor = AppTheme.colors.neutral000, - modifier = modifier - .padding(horizontal = PaddingDefaults.Medium) - .fillMaxWidth(), - shape = RoundedCornerShape(SizeDefaults.one) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - enabled = true, - onClick = onCardSelected - ) - .padding(PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - startIcon, - null, - tint = AppTheme.colors.primary600 - ) - - Text( - text, - style = AppTheme.typography.subtitle1, - modifier = modifier - .weight(1f) - .padding(start = PaddingDefaults.Medium) - ) - - Icon( - checkIcon, - null, - tint = checkIconTint - ) - } - } -} - -@Preview("SelectableCard") -@Composable -private fun PreviewSelectableCard() { - AppTheme { - SelectableCard( - startIcon = Icons.Rounded.Check, - text = "Info here", - selected = true - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/DomainVerifierBanner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/DomainVerifierBanner.kt deleted file mode 100644 index 8b6bb1d6..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/DomainVerifierBanner.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.BannerClickableTextIcon -import de.gematik.ti.erp.app.utils.compose.BannerIcon -import de.gematik.ti.erp.app.utils.compose.ErezeptBanner -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme - -@Composable -fun DomainVerifierBanner( - onClick: () -> Unit, - onClose: () -> Unit -) { - ErezeptBanner( - contentColor = AppTheme.colors.neutral999, - containerColor = AppTheme.colors.primary100, - title = stringResource(R.string.domain_verifier_banner_title), - text = stringResource(R.string.domain_verifier_dialog_body), - bottomIcon = BannerClickableTextIcon( - text = stringResource(R.string.domain_verifier_dialog_button), - icon = BannerIcon.Custom( - Icons.Default.ArrowForward, - AppTheme.colors.primary600 - ) - ) { - onClick() - }, - onClickClose = onClose - ) -} - -@LightDarkPreview -@Composable -fun DomainVerifierBannerPreview() { - PreviewAppTheme { - DomainVerifierBanner( - onClick = {}, - onClose = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAnimation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAnimation.kt index 380793c1..f7f1d6a6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAnimation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAnimation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.ui.components diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthDialog.kt index 76fb4dc7..9f35932c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthDialog.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.cardwall.ui.components import android.content.Intent @@ -27,6 +29,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight @@ -58,8 +61,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.trackAuth @@ -264,7 +265,7 @@ fun CardWallAuthenticationDialog( AuthenticationState.HealthCardPin2RetriesLeft, AuthenticationState.HealthCardPin1RetryLeft -> onRetryPin() AuthenticationState.HealthCardBlocked -> onUnlockEgk() - else -> if (cardWallController.checkNfcEnabled()) { + else -> if (cardWallController.isNfcEnabled) { coroutineScope.launch { toggleAuth.emit(ToggleAuth.ToggleByUser(true)) } @@ -386,93 +387,85 @@ private fun AuthenticationDialog( onCancel: () -> Unit, onClickTroubleshooting: (() -> Unit)? = null ) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties( - dismissOnBackPress = false, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - decorFitsSystemWindows = false - ) + Box( + Modifier + .testTag(TestTag.CardWall.Nfc.CardReadingDialog) + .semantics(false) { } + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomCenter ) { - Box( - Modifier - .testTag(TestTag.CardWall.Nfc.CardReadingDialog) - .semantics(false) { } - .fillMaxSize() - .systemBarsPadding(), - contentAlignment = Alignment.BottomCenter + Surface( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(28.dp), + elevation = 8.dp ) { - Surface( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(PaddingDefaults.Medium), - color = MaterialTheme.colors.surface, - shape = RoundedCornerShape(28.dp), - elevation = 8.dp - ) { - val screen = remember(state) { - when (state) { - AuthenticationState.AuthenticationFlowInitialized -> 0 - AuthenticationState.HealthCardCommunicationChannelReady, - AuthenticationState.HealthCardCommunicationTrustedChannelEstablished, - AuthenticationState.HealthCardCommunicationCertificateLoaded, - AuthenticationState.HealthCardCommunicationFinished, - AuthenticationState.IDPCommunicationFinished, - AuthenticationState.AuthenticationFlowFinished -> 1 - AuthenticationState.HealthCardCommunicationInterrupted -> 2 - else -> 1 - } + val screen = remember(state) { + when (state) { + AuthenticationState.AuthenticationFlowInitialized -> 0 + AuthenticationState.HealthCardCommunicationChannelReady, + AuthenticationState.HealthCardCommunicationTrustedChannelEstablished, + AuthenticationState.HealthCardCommunicationCertificateLoaded, + AuthenticationState.HealthCardCommunicationFinished, + AuthenticationState.IDPCommunicationFinished, + AuthenticationState.AuthenticationFlowFinished -> 1 + + AuthenticationState.HealthCardCommunicationInterrupted -> 2 + else -> 1 } + } - Column( - modifier = Modifier - .padding(24.dp) - .wrapContentSize() - .testTag("cdw_auth_nfc_bottom_sheet"), - verticalArrangement = Arrangement.spacedBy(8.dp) + Column( + modifier = Modifier + .padding(24.dp) + .wrapContentSize() + .testTag("cdw_auth_nfc_bottom_sheet"), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + enabled = onCancelEnabled, + onClick = onCancel ) { - TextButton( - enabled = onCancelEnabled, - onClick = onCancel - ) { - Text(stringResource(R.string.cdw_nfc_dlg_cancel).uppercase(Locale.getDefault())) - } - CardAnimationBox(screen) - - // how to hold your card - val rotatingScanCardAssistance = rotatingScanCardAssistance() + Text(stringResource(R.string.cdw_nfc_dlg_cancel).uppercase(Locale.getDefault())) + } + CardAnimationBox(screen) - var info by remember { mutableStateOf(rotatingScanCardAssistance.first()) } + // how to hold your card + val rotatingScanCardAssistance = rotatingScanCardAssistance() - LaunchedEffect(state) { - if (state == AuthenticationState.AuthenticationFlowInitialized) { - var i = 0 - while (true) { - info = rotatingScanCardAssistance[i] + var info by remember { mutableStateOf(rotatingScanCardAssistance.first()) } - i = if (i < rotatingScanCardAssistance.size - 1) { - i + 1 - } else { - 0 - } + LaunchedEffect(state) { + if (state == AuthenticationState.AuthenticationFlowInitialized) { + var i = 0 + while (true) { + info = rotatingScanCardAssistance[i] - delay(5000) + i = if (i < rotatingScanCardAssistance.size - 1) { + i + 1 + } else { + 0 } + + delay(5000) } } + } - info = extractInfo(state) ?: info + info = extractInfo(state) ?: info - InfoText( - showTroubleshooting, - info, - onClickTroubleshooting = { - onClickTroubleshooting?.run { onClickTroubleshooting() } - } - ) - } + InfoText( + showTroubleshooting, + info, + onClickTroubleshooting = { + onClickTroubleshooting?.run { onClickTroubleshooting() } + } + ) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthenticationData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthenticationData.kt index 1f8d2671..7aff1953 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthenticationData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallAuthenticationData.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.ui.components import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.cardwall.presentation.AltPairingProvider +import de.gematik.ti.erp.app.cardwall.presentation.SaveCredentialsController @Stable sealed class CardWallAuthenticationData( @@ -37,10 +37,10 @@ sealed class CardWallAuthenticationData( ) @Stable - class AltPairingWithHealthCard( + class SaveCredentialsWithHealthCard( cardAccessNumber: String, personalIdentificationNumber: String, - val initialPairingData: AltPairingProvider.AuthResult.Initialized + val initialPairingData: SaveCredentialsController.AuthResult.Initialized ) : CardWallAuthenticationData( cardAccessNumber = cardAccessNumber, personalIdentificationNumber = personalIdentificationNumber diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenBannerSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenBannerSection.kt deleted file mode 100644 index a57f7582..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenBannerSection.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui.components - -import android.os.Build -import androidx.compose.foundation.lazy.LazyListScope -import de.gematik.ti.erp.app.cardwall.ui.DomainVerifierBanner -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -@Suppress("FunctionName") -fun LazyListScope.CardWallExternalAuthenticationScreenBannerSection( - showBanner: Boolean, - onClickBanner: () -> Unit, - onClose: () -> Unit -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && showBanner) { - item { - DomainVerifierBanner( - onClick = onClickBanner, - onClose = onClose - ) - } - item { - SpacerMedium() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenHeaderSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenHeaderSection.kt deleted file mode 100644 index d6941568..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallExternalAuthenticationScreenHeaderSection.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui.components - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Search -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -@Suppress("FunctionName") -@OptIn(ExperimentalFoundationApi::class) -fun LazyListScope.CardWallExternalAuthenticationScreenHeaderSection( - modifier: Modifier = Modifier, - searchValue: TextFieldValue, - focusRequester: FocusRequester, - onValueChange: (TextFieldValue) -> Unit -) { - stickyHeader { - Column( - modifier = Modifier - .background(AppTheme.colors.neutral000) - .then(modifier) - ) { - Text( - stringResource(R.string.cdw_fasttrack_choose_insurance), - style = MaterialTheme.typography.h6 - ) - SpacerSmall() - Text( - stringResource(R.string.cdw_fasttrack_help_info), - style = AppTheme.typography.body2l - ) - SpacerLarge() - SearchField( - value = searchValue, - focusRequester = focusRequester, - onValueChange = { - onValueChange(it) - } - ) - SpacerMedium() - } - } -} - -@Composable -private fun SearchField( - value: TextFieldValue, - focusRequester: FocusRequester, - onValueChange: (TextFieldValue) -> Unit -) = - OutlinedTextField( - value = value, - onValueChange = { - onValueChange(it) - }, - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .onFocusChanged { - if (it.isFocused) { - focusRequester.requestFocus() - } - } - .focusRequester(focusRequester), - placeholder = { - Text( - stringResource(R.string.cdw_fasttrack_search_placeholder), - style = AppTheme.typography.body1l - ) - }, - shape = RoundedCornerShape(PaddingDefaults.Medium), - leadingIcon = { Icon(Icons.Rounded.Search, null) }, - colors = TextFieldDefaults.outlinedTextFieldColors( - backgroundColor = AppTheme.colors.neutral100, - placeholderColor = AppTheme.colors.neutral600, - leadingIconColor = AppTheme.colors.neutral600, - focusedBorderColor = Color.Unspecified, - unfocusedBorderColor = Color.Unspecified, - disabledBorderColor = Color.Unspecified, - errorBorderColor = Color.Unspecified - ) - ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallScaffold.kt index d6951092..c40dd419 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/CardWallScaffold.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ // Added to allow composable extension functions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GematikErrorDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GematikErrorDialog.kt index 2dd8f862..22068a53 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GematikErrorDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GematikErrorDialog.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.ui.components @@ -29,11 +29,11 @@ import de.gematik.ti.erp.app.idp.model.error.GematikResponseError import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.emptyResponseError import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.prettyErrorCode import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.prettyErrorText +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog import de.gematik.ti.erp.app.utils.compose.ErezeptText import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme private const val MARQUEE_DELAY = 2000 diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidItem.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidItem.kt new file mode 100644 index 00000000..337edd0e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.idp.model.HealthInsuranceData +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun GidItem( + profileId: String, + healthInsuranceData: HealthInsuranceData, + onClickHealthInsuranceIdp: (String, HealthInsuranceData) -> Unit, + onNotGid: () -> Unit +) { + Text( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth() + .clickable { + if (healthInsuranceData.isGid) { + onClickHealthInsuranceIdp(profileId, healthInsuranceData) + } else { + onNotGid() + } + }, + text = healthInsuranceData.name + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidScreenHeaderSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidScreenHeaderSection.kt new file mode 100644 index 00000000..a486aa56 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/GidScreenHeaderSection.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowForward +import androidx.compose.material.icons.rounded.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.erezeptTextFieldColors +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("FunctionName") +@Composable +fun GidScreenHeaderSection( + modifier: Modifier = Modifier, + searchValue: TextFieldValue, + focusRequester: FocusRequester, + onValueChange: (TextFieldValue) -> Unit, + onNavigateToHelp: () -> Unit +) { + ExternalAuthenticationScreenHeader( + modifier = modifier, + searchValue = searchValue, + focusRequester = focusRequester, + onValueChange = onValueChange, + onNavigateToHelp = onNavigateToHelp + ) +} + +@Composable +private fun ExternalAuthenticationScreenHeader( + modifier: Modifier = Modifier, + searchValue: TextFieldValue, + focusRequester: FocusRequester, + onValueChange: (TextFieldValue) -> Unit, + onNavigateToHelp: () -> Unit +) { + Column( + modifier = Modifier + .background(AppTheme.colors.neutral000) + .then(modifier) + ) { + Text( + stringResource(R.string.cardwall_gid_header), + style = AppTheme.typography.h6 + ) + SpacerSmall() + Text( + stringResource(R.string.cardwall_gid_body), + style = AppTheme.typography.body2l + ) + SpacerSmall() + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = onNavigateToHelp, + content = { + Text( + stringResource(R.string.cardwall_gid_help_button), + style = AppTheme.typography.body1 + ) + SpacerTiny() + Icon( + Icons.AutoMirrored.Rounded.ArrowForward, + contentDescription = null, + tint = AppTheme.colors.primary600 + ) + } + ) + SpacerLarge() + SearchField( + value = searchValue, + focusRequester = focusRequester, + onValueChange = { + onValueChange(it) + } + ) + SpacerMedium() + } +} + +@Composable +private fun SearchField( + value: TextFieldValue, + focusRequester: FocusRequester, + onValueChange: (TextFieldValue) -> Unit +) = + ErezeptOutlineText( + value = value, + onValueChange = { + onValueChange(it) + }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { + if (it.isFocused) { + focusRequester.requestFocus() + } + } + .focusRequester(focusRequester), + placeholder = { + Text( + stringResource(R.string.cdw_fasttrack_search_placeholder), + style = AppTheme.typography.body1l + ) + }, + shape = RoundedCornerShape(PaddingDefaults.Medium), + leadingIcon = { Icon(Icons.Rounded.Search, null) }, + colors = erezeptTextFieldColors( + focusedLeadingIconColor = AppTheme.colors.neutral600, + unfocusedLeadingIconColor = AppTheme.colors.neutral600, + focusedContainerColor = AppTheme.colors.neutral100, + unfocusedContainerColor = AppTheme.colors.neutral100, + focusedPlaceholderColor = AppTheme.colors.neutral600, + unfocusedPlaceholderColor = AppTheme.colors.neutral600, + focussedBorderColor = Color.Unspecified, + unfocusedBorderColor = Color.Unspecified, + disabledBorderColor = Color.Unspecified, + errorBorderColor = Color.Unspecified + ) + ) + +@LightDarkPreview +@Composable +fun CardWallExternalAuthenticationScreenHeaderPreview() { + PreviewAppTheme { + ExternalAuthenticationScreenHeader( + searchValue = TextFieldValue(""), + focusRequester = FocusRequester(), + onValueChange = {}, + onNavigateToHelp = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreen.kt deleted file mode 100644 index f8ceea3c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreen.kt +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.HelpOutline -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.LottieConstants -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallNfcPositionState -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.NavigationBack -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.TopAppBar -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import kotlin.math.PI -import kotlin.math.cos - -private const val LowerPos = 1 / 3f -private const val HigherPos = 2 / 3f -private val PosRange = LowerPos..HigherPos - -@Composable -fun ReadCardScreenComposable( - onBack: () -> Unit, - onClickTroubleshooting: () -> Unit -) { - val nfcPositionState = rememberCardWallNfcPositionState() - val state = nfcPositionState.state - val systemUiController = rememberSystemUiController() - - var phoneImgSize by remember { mutableStateOf(IntSize.Zero) } - var titleHeight by remember { mutableStateOf(0) } - var subTitleHeight by remember { mutableStateOf(0) } - var descriptionHeight by remember { mutableStateOf(0) } - val nfcXPos by remember { mutableStateOf((state.nfcData.nfcPos.x0 + state.nfcData.nfcPos.x1) / 2) } - val nfcYPos by remember { mutableStateOf((state.nfcData.nfcPos.y0 + state.nfcData.nfcPos.y1) / 2) } - - val useDarkIcons = MaterialTheme.colors.isLight - val lazyListState = rememberLazyListState() - AppTheme( - darkTheme = true - ) { - DisposableEffect(Unit) { - systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = false) - onDispose { systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = useDarkIcons) } - } - Scaffold( - modifier = Modifier.testTag(TestTag.CardWall.Nfc.NfcScreen), - topBar = { - TopAppBar( - title = {}, - backgroundColor = MaterialTheme.colors.background, - modifier = Modifier, - navigationIcon = { NavigationBack { onBack() } }, - actions = { TroubleShootingButton(onTroubleshooting = onClickTroubleshooting) } - ) - } - ) { innerPadding -> - ReadCardScreenContent( - lazyListState = lazyListState, - innerPadding = innerPadding, - nfcXPos = nfcXPos, - nfcYPos = nfcYPos, - phoneImgSize = phoneImgSize, - onPhoneSizeChange = { phoneImgSize = it }, - titleHeight = titleHeight, - onTitleHeightChange = { titleHeight = it }, - subTitleHeight = subTitleHeight, - onSubTitleHeightChange = { subTitleHeight = it }, - descriptionHeight = descriptionHeight, - onDescriptionHeightChange = { descriptionHeight = it } - ) - } - } -} - -@Suppress("MagicNumber") -@Composable -private fun ReadCardScreenContent( - lazyListState: LazyListState, - innerPadding: PaddingValues, - nfcXPos: Double, - nfcYPos: Double, - phoneImgSize: IntSize, - onPhoneSizeChange: (IntSize) -> Unit, - titleHeight: Int, - onTitleHeightChange: (Int) -> Unit, - subTitleHeight: Int, - onSubTitleHeightChange: (Int) -> Unit, - descriptionHeight: Int, - onDescriptionHeightChange: (Int) -> Unit -) { - LazyColumn( - state = lazyListState, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .navigationBarsPadding() - .semantics(mergeDescendants = true) {}, - verticalArrangement = Arrangement.SpaceBetween - ) { - item { - Text( - stringResource(R.string.nfc_instruction_headline), - style = AppTheme.typography.h6, - modifier = Modifier - .padding( - all = PaddingDefaults.Medium - ) - .onGloballyPositioned { onTitleHeightChange(it.size.height) } - .fillMaxWidth(), - textAlign = TextAlign.Center - ) - - AnimatedVisibility( - modifier = Modifier.fillMaxWidth(), - visible = with(LocalDensity.current) { - (1.5f * phoneImgSize.height + titleHeight + subTitleHeight + descriptionHeight).toDp() - } <= LocalConfiguration.current.screenHeightDp.dp - ) { - Text( - stringResource(R.string.nfc_instruction_time_hint), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center, - modifier = Modifier - .padding( - bottom = PaddingDefaults.Large, - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - .onGloballyPositioned { onSubTitleHeightChange(it.size.height) } - .fillMaxWidth() - ) - } - } - item { - CardOnPhone( - nfcXPos = nfcXPos, - nfcYPos = nfcYPos, - phoneImgSize = phoneImgSize, - onPhoneSizeChanged = { onPhoneSizeChange(it) } - ) - } - item { - val cardPosDescr = when { - nfcXPos < LowerPos && nfcYPos < LowerPos -> stringResource( - R.string.nfc_instruction_chip_location_top_left - ) - nfcXPos < LowerPos && nfcYPos in PosRange -> stringResource( - R.string.nfc_instruction_chip_location_middle_left - ) - nfcXPos < LowerPos && nfcYPos > HigherPos -> stringResource( - R.string.nfc_instruction_chip_location_bot_left - ) - nfcXPos in PosRange && nfcYPos < LowerPos -> stringResource( - R.string.nfc_instruction_chip_location_top_central - ) - nfcXPos in PosRange && nfcYPos in PosRange -> stringResource( - R.string.nfc_instruction_chip_location_middle - ) - nfcXPos in PosRange && nfcYPos > HigherPos -> stringResource( - R.string.nfc_instruction_chip_location_bot_central - ) - nfcXPos > HigherPos && nfcYPos < LowerPos -> stringResource( - R.string.nfc_instruction_chip_location_top_right - ) - nfcXPos > HigherPos && nfcYPos in PosRange -> stringResource( - R.string.nfc_instruction_chip_location_middle_right - ) - nfcXPos > HigherPos && nfcYPos > HigherPos -> stringResource( - R.string.nfc_instruction_chip_location_bot_right - ) - else -> "" - } - AnimatedVisibility( - modifier = Modifier.fillMaxWidth(), - visible = with(LocalDensity.current) { - (1.5f * phoneImgSize.height + titleHeight + subTitleHeight + descriptionHeight).toDp() - } <= LocalConfiguration.current.screenHeightDp.dp - ) { - Text( - annotatedStringResource( - R.string.nfc_instruction_chip_location, - cardPosDescr - ), - style = AppTheme.typography.subtitle2l, - modifier = Modifier - .padding( - vertical = PaddingDefaults.Large, - horizontal = PaddingDefaults.Medium - ) - .onGloballyPositioned { onDescriptionHeightChange(it.size.height) } - .fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } - } -} - -@Composable -private fun TroubleShootingButton(onTroubleshooting: () -> Unit) { - Button( - onClick = onTroubleshooting, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny), - enabled = true, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.neutral050, - contentColor = AppTheme.colors.primary600 - ) - ) { - Icon(Icons.Rounded.HelpOutline, contentDescription = null) - SpacerTiny() - Text( - stringResource(R.string.nfc_instruction_help_button), - textAlign = TextAlign.Center - ) - } -} - -@Suppress("MagicNumber") -@Composable -private fun CardOnPhone( - nfcXPos: Double, - nfcYPos: Double, - phoneImgSize: IntSize, - onPhoneSizeChanged: (IntSize) -> Unit -) { - BoxWithConstraints( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - /* - The first part of the offset calculation (in front of +) is the shift in the x-axis - (x-shift: 1/3 * x * max width & 1/3 * x * max height). - The second part of the offset calculation (after +) is the shift in the y-axis - (y-shift: -2/3 * y * max width & 2/3 * y * max height). - Cos-function is used to swap the offset direction (cos(0 pi) = 1, cos(1 pi) = -1), - since the images are centered all functions were halved and inverted. - */ - CardAndAnimation( - modifier = Modifier.offset { - IntOffset( - x = ( - (phoneImgSize.width * -cos(nfcXPos * PI) / 6) + - (phoneImgSize.width * cos(nfcYPos * PI) / 3) - ).toInt(), - y = ( - (phoneImgSize.height * -cos(nfcXPos * PI).toFloat() / 6) + - (phoneImgSize.height * -cos(nfcYPos * PI).toFloat() / 3) - ).toInt() - ) - } - ) - PhoneWithScaling(modifier = Modifier.width(maxWidth * 2 / 3), onPhoneSizeChanged = onPhoneSizeChanged) - } -} - -@Composable -private fun CardAndAnimation(modifier: Modifier) { - val animationComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.animation_pulse_lottie)) - val healthCardLottie = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.healthcard_lottie)) - val progress by animateLottieCompositionAsState( - animationComposition.value, - iterations = LottieConstants.IterateForever - ) - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - LottieAnimation( - animationComposition.value, - progress = { progress } - ) - LottieAnimation( - healthCardLottie.value - ) - } -} - -@Composable -private fun PhoneWithScaling(modifier: Modifier, onPhoneSizeChanged: (IntSize) -> Unit) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - val phoneLottie = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.device_lottie)) - LottieAnimation( - phoneLottie.value, - contentScale = ContentScale.FillWidth, - modifier = Modifier.onGloballyPositioned { - onPhoneSizeChanged(it.size) - } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreenScaffold.kt new file mode 100644 index 00000000..24670308 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/components/ReadCardScreenScaffold.kt @@ -0,0 +1,360 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.HelpOutline +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.cardwall.usecase.model.NfcPositionUseCaseData +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.NavigateBackButton +import de.gematik.ti.erp.app.utils.compose.TopAppBar +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import kotlin.math.PI +import kotlin.math.cos + +private const val LowerPos = 1 / 3f +private const val HigherPos = 2 / 3f +private val PosRange = LowerPos..HigherPos + +@Composable +fun ReadCardScreenScaffold( + onBack: () -> Unit, + onClickTroubleshooting: () -> Unit, + nfcPosition: NfcPositionUseCaseData.NfcPos +) { + val systemUiController = rememberSystemUiController() + + var phoneImgSize by remember { mutableStateOf(IntSize.Zero) } + var titleHeight by remember { mutableStateOf(0) } + var subTitleHeight by remember { mutableStateOf(0) } + var descriptionHeight by remember { mutableStateOf(0) } + val nfcXPos by remember { mutableStateOf((nfcPosition.x0 + nfcPosition.x1) / 2) } + val nfcYPos by remember { mutableStateOf((nfcPosition.y0 + nfcPosition.y1) / 2) } + + val useDarkIcons = MaterialTheme.colors.isLight + val lazyListState = rememberLazyListState() + AppTheme( + darkTheme = true + ) { + DisposableEffect(Unit) { + systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = false) + onDispose { systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = useDarkIcons) } + } + Scaffold( + modifier = Modifier.testTag(TestTag.CardWall.Nfc.NfcScreen), + topBar = { + TopAppBar( + title = {}, + backgroundColor = MaterialTheme.colors.background, + modifier = Modifier, + navigationIcon = { NavigateBackButton { onBack() } }, + actions = { TroubleShootingButton(onTroubleshooting = onClickTroubleshooting) } + ) + } + ) { innerPadding -> + ReadCardScreenContent( + lazyListState = lazyListState, + innerPadding = innerPadding, + nfcXPos = nfcXPos, + nfcYPos = nfcYPos, + phoneImgSize = phoneImgSize, + onPhoneSizeChange = { phoneImgSize = it }, + titleHeight = titleHeight, + onTitleHeightChange = { titleHeight = it }, + subTitleHeight = subTitleHeight, + onSubTitleHeightChange = { subTitleHeight = it }, + descriptionHeight = descriptionHeight, + onDescriptionHeightChange = { descriptionHeight = it } + ) + } + } +} + +@Suppress("MagicNumber", "CyclomaticComplexMethod") +@Composable +private fun ReadCardScreenContent( + lazyListState: LazyListState, + innerPadding: PaddingValues, + nfcXPos: Double, + nfcYPos: Double, + phoneImgSize: IntSize, + onPhoneSizeChange: (IntSize) -> Unit, + titleHeight: Int, + onTitleHeightChange: (Int) -> Unit, + subTitleHeight: Int, + onSubTitleHeightChange: (Int) -> Unit, + descriptionHeight: Int, + onDescriptionHeightChange: (Int) -> Unit +) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .navigationBarsPadding() + .semantics(mergeDescendants = true) {}, + verticalArrangement = Arrangement.SpaceBetween + ) { + item { + Text( + stringResource(R.string.nfc_instruction_headline), + style = AppTheme.typography.h6, + modifier = Modifier + .padding( + all = PaddingDefaults.Medium + ) + .onGloballyPositioned { onTitleHeightChange(it.size.height) } + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + visible = with(LocalDensity.current) { + (1.5f * phoneImgSize.height + titleHeight + subTitleHeight + descriptionHeight).toDp() + } <= LocalConfiguration.current.screenHeightDp.dp + ) { + Text( + stringResource(R.string.nfc_instruction_time_hint), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center, + modifier = Modifier + .padding( + bottom = PaddingDefaults.Large, + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + .onGloballyPositioned { onSubTitleHeightChange(it.size.height) } + .fillMaxWidth() + ) + } + } + item { + CardOnPhone( + nfcXPos = nfcXPos, + nfcYPos = nfcYPos, + phoneImgSize = phoneImgSize, + onPhoneSizeChanged = { onPhoneSizeChange(it) } + ) + } + item { + val cardPosDescr = when { + nfcXPos < LowerPos && nfcYPos < LowerPos -> stringResource( + R.string.nfc_instruction_chip_location_top_left + ) + nfcXPos < LowerPos && nfcYPos in PosRange -> stringResource( + R.string.nfc_instruction_chip_location_middle_left + ) + nfcXPos < LowerPos && nfcYPos > HigherPos -> stringResource( + R.string.nfc_instruction_chip_location_bot_left + ) + nfcXPos in PosRange && nfcYPos < LowerPos -> stringResource( + R.string.nfc_instruction_chip_location_top_central + ) + nfcXPos in PosRange && nfcYPos in PosRange -> stringResource( + R.string.nfc_instruction_chip_location_middle + ) + nfcXPos in PosRange && nfcYPos > HigherPos -> stringResource( + R.string.nfc_instruction_chip_location_bot_central + ) + nfcXPos > HigherPos && nfcYPos < LowerPos -> stringResource( + R.string.nfc_instruction_chip_location_top_right + ) + nfcXPos > HigherPos && nfcYPos in PosRange -> stringResource( + R.string.nfc_instruction_chip_location_middle_right + ) + nfcXPos > HigherPos && nfcYPos > HigherPos -> stringResource( + R.string.nfc_instruction_chip_location_bot_right + ) + else -> "" + } + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + visible = with(LocalDensity.current) { + (1.5f * phoneImgSize.height + titleHeight + subTitleHeight + descriptionHeight).toDp() + } <= LocalConfiguration.current.screenHeightDp.dp + ) { + Text( + annotatedStringResource( + R.string.nfc_instruction_chip_location, + cardPosDescr + ), + style = AppTheme.typography.subtitle2l, + modifier = Modifier + .padding( + vertical = PaddingDefaults.Large, + horizontal = PaddingDefaults.Medium + ) + .onGloballyPositioned { onDescriptionHeightChange(it.size.height) } + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } + } +} + +@Composable +private fun TroubleShootingButton(onTroubleshooting: () -> Unit) { + Button( + onClick = onTroubleshooting, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny), + enabled = true, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = AppTheme.colors.neutral050, + contentColor = AppTheme.colors.primary600 + ) + ) { + Icon(Icons.Rounded.HelpOutline, contentDescription = null) + SpacerTiny() + Text( + stringResource(R.string.nfc_instruction_help_button), + textAlign = TextAlign.Center + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun CardOnPhone( + nfcXPos: Double, + nfcYPos: Double, + phoneImgSize: IntSize, + onPhoneSizeChanged: (IntSize) -> Unit +) { + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + /* + The first part of the offset calculation (in front of +) is the shift in the x-axis + (x-shift: 1/3 * x * max width & 1/3 * x * max height). + The second part of the offset calculation (after +) is the shift in the y-axis + (y-shift: -2/3 * y * max width & 2/3 * y * max height). + Cos-function is used to swap the offset direction (cos(0 pi) = 1, cos(1 pi) = -1), + since the images are centered all functions were halved and inverted. + */ + CardAndAnimation( + modifier = Modifier.offset { + IntOffset( + x = ( + (phoneImgSize.width * -cos(nfcXPos * PI) / 6) + + (phoneImgSize.width * cos(nfcYPos * PI) / 3) + ).toInt(), + y = ( + (phoneImgSize.height * -cos(nfcXPos * PI).toFloat() / 6) + + (phoneImgSize.height * -cos(nfcYPos * PI).toFloat() / 3) + ).toInt() + ) + } + ) + PhoneWithScaling(modifier = Modifier.width(maxWidth * 2 / 3), onPhoneSizeChanged = onPhoneSizeChanged) + } +} + +@Composable +private fun CardAndAnimation(modifier: Modifier) { + val animationComposition = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.animation_pulse_lottie)) + val healthCardLottie = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.healthcard_lottie)) + val progress by animateLottieCompositionAsState( + animationComposition.value, + iterations = LottieConstants.IterateForever + ) + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + LottieAnimation( + animationComposition.value, + progress = { progress } + ) + LottieAnimation( + healthCardLottie.value + ) + } +} + +@Composable +private fun PhoneWithScaling(modifier: Modifier, onPhoneSizeChanged: (IntSize) -> Unit) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + val phoneLottie = rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.device_lottie)) + LottieAnimation( + phoneLottie.value, + contentScale = ContentScale.FillWidth, + modifier = Modifier.onGloballyPositioned { + onPhoneSizeChanged(it.size) + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallIntroScreenContentPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallIntroScreenContentPreviewParameterProvider.kt new file mode 100644 index 00000000..491c1924 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallIntroScreenContentPreviewParameterProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +data class CardWallIntroScreenPreviewData( + val name: String, + val isNfcAvailable: Boolean, + val isNfcEnabled: Boolean, + val isDomainVerified: Boolean +) + +class CardWallIntroScreenContentPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf(nfcNotAvailable, nfcNotEnabled, nfcEnabled) + + companion object { + val nfcNotAvailable = CardWallIntroScreenPreviewData( + name = "nfcNotAvailable", + isNfcAvailable = false, + isNfcEnabled = false, + isDomainVerified = false + ) + + val nfcNotEnabled = CardWallIntroScreenPreviewData( + name = "nfcNotEnabled", + isNfcAvailable = true, + isNfcEnabled = false, + isDomainVerified = false + ) + + val nfcEnabled = CardWallIntroScreenPreviewData( + name = "nfcEnabled", + isNfcAvailable = true, + isNfcEnabled = true, + isDomainVerified = false + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallPinScreenPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallPinScreenPreviewParameterProvider.kt new file mode 100644 index 00000000..6c48e34e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallPinScreenPreviewParameterProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +data class CardWallPinScreenPreviewData( + val name: String, + val pin: String +) + +class CardWallPinScreenPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf(pinWith3Digits, pinWith6Digits) + + companion object { + val pinWith3Digits = CardWallPinScreenPreviewData( + name = "pinWith3Digits", + pin = "123" + ) + + val pinWith6Digits = CardWallPinScreenPreviewData( + name = "pinWith5Digits", + pin = "123456" + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallSaveCredentialsPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallSaveCredentialsPreviewParameterProvider.kt new file mode 100644 index 00000000..31667611 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/CardWallSaveCredentialsPreviewParameterProvider.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.cardwall.ui.screens.AuthenticationMethod + +data class CardWallSaveCredentialsScreenPreviewData( + val name: String, + val selectedAuthMode: AuthenticationMethod, + val showFutureLogOutHint: Boolean +) + +class CardWallSaveCredentialsPreviewParameterProvider : + PreviewParameterProvider { + override val values = sequenceOf( + CardWallSaveCredentialsScreenPreviewData( + name = "AuthenticationMethod.None", + selectedAuthMode = AuthenticationMethod.None, + showFutureLogOutHint = true + ), + CardWallSaveCredentialsScreenPreviewData( + name = "AuthenticationMethod.Alternative", + selectedAuthMode = AuthenticationMethod.Alternative, + showFutureLogOutHint = false + ), + CardWallSaveCredentialsScreenPreviewData( + name = "AuthenticationMethod.HealthCard", + selectedAuthMode = AuthenticationMethod.HealthCard, + showFutureLogOutHint = true + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/HealthInsuranceDataPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/HealthInsuranceDataPreviewParameterProvider.kt new file mode 100644 index 00000000..0ff240b9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/HealthInsuranceDataPreviewParameterProvider.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.idp.model.HealthInsuranceData +import de.gematik.ti.erp.app.utils.uistate.UiState + +class HealthInsuranceDataPreviewParameterProvider : PreviewParameterProvider>> { + override val values = sequenceOf( + UiState.Loading(), + UiState.Empty(), + UiState.Error(Throwable("Error")), + UiState.Data( + listOf( + HealthInsuranceData( + name = "Insurance 1", + id = "1", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 2", + id = "2", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 3", + id = "3", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 4", + id = "4", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 5", + id = "5", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 6", + id = "6", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 7", + id = "7", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 8", + id = "8", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 9", + id = "9", + isGid = true, + logo = null + ), + HealthInsuranceData( + name = "Insurance 10", + id = "10", + isGid = true, + logo = null + ) + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/NfcPositionPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/NfcPositionPreviewParameter.kt new file mode 100644 index 00000000..5633d75f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/preview/NfcPositionPreviewParameter.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.cardwall.usecase.model.NfcPositionUseCaseData + +data class NfcPositionPreview( + val name: String, + val nfcPos: NfcPositionUseCaseData.NfcPos +) + +class NfcPositionPreviewParameter : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NfcPositionPreview( + "top-left", + NfcPositionUseCaseData.NfcPos( + x0 = 0.0, + y0 = 0.0, + x1 = 0.0, + y1 = 0.0 + ) + ), + NfcPositionPreview( + "top-mid", + NfcPositionUseCaseData.NfcPos( + x0 = 0.5, + y0 = 0.0, + x1 = 0.5, + y1 = 0.0 + ) + ), + NfcPositionPreview( + "top-right", + NfcPositionUseCaseData.NfcPos( + x0 = 1.0, + y0 = 0.0, + x1 = 1.0, + y1 = 0.0 + ) + ), + NfcPositionPreview( + "center-mid", + NfcPositionUseCaseData.NfcPos( + x0 = 0.5, + y0 = 0.5, + x1 = 0.5, + y1 = 0.5 + ) + ), + NfcPositionPreview( + "bottom-right", + NfcPositionUseCaseData.NfcPos( + x0 = 1.0, + y0 = 1.0, + x1 = 1.0, + y1 = 1.0 + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreen.kt new file mode 100644 index 00000000..9d939f05 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreen.kt @@ -0,0 +1,277 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.CanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus + +const val CAN_LENGTH = 6 + +@Requirement( + "O.Data_6#9", + sourceSpecification = "BSI-eRp-ePA", + rationale = ".. the screen where CAN is entered from." +) +@Requirement( + "O.Purp_2#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The CAN is needed for creating the secure channel to the eGK." +) +class CardWallCanScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val can by graphController.can.collectAsStateWithLifecycle() + val lazyListState = rememberLazyListState() + BackHandler { + navController.popBackStack() + } + + CardWallScaffold( + modifier = Modifier.testTag(TestTag.CardWall.CAN.CANScreen), + onBack = { + navController.popBackStack() + }, + title = stringResource(R.string.cdw_top_bar_title), + nextEnabled = can.length == CAN_LENGTH, + onNext = { + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = "", + can = "" + ) + ) + }, + listState = lazyListState, + nextText = stringResource(R.string.unlock_egk_next), + actions = { + TextButton( + onClick = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + } + ) { + Text(stringResource(R.string.cancel)) + } + } + ) { innerPadding -> + CanScreenContent( + lazyListState = lazyListState, + innerPadding = innerPadding, + can = can, + onClickLearnMore = { + navController.navigate( + OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.path() + ) + }, + onNext = { + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = "", + can = "" + ) + ) + }, + onCanChange = { graphController.setCardAccessNumber(it) } + ) + } + } +} + +@Composable +fun CanScreenContent( + lazyListState: LazyListState, + innerPadding: PaddingValues, + onClickLearnMore: () -> Unit, + onNext: () -> Unit, + can: String, + onCanChange: (String) -> Unit +) { + val contentPadding by rememberContentPadding(innerPadding) + + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding + ) { + item { + HealthCardCanImage() + } + item { + CanDescription(onClickLearnMore) + SpacerXXLarge() + } + item { + CanInputField( + modifier = Modifier.scrollOnFocus(to = 2, lazyListState), + can = can, + onCanChange = onCanChange, + next = onNext + ) + } + } +} + +@Composable +fun HealthCardCanImage() { + Column(modifier = Modifier.wrapContentHeight()) { + Image( + painterResource(R.drawable.card_wall_card_can), + null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + SpacerXXLarge() + } +} + +@Composable +fun CanDescription(onClickLearnMore: () -> Unit) { + Column { + Text( + stringResource(R.string.cdw_can_headline), + style = AppTheme.typography.h5 + ) + SpacerSmall() + Text( + stringResource(R.string.cdw_can_description), + style = AppTheme.typography.body1 + ) + SpacerSmall() + ClickableTaggedText( + text = annotatedLinkStringLight( + uri = "", + text = stringResource(R.string.cdw_no_can_on_card) + ), + onClick = { onClickLearnMore() }, + style = AppTheme.typography.body2, + modifier = Modifier + .align(Alignment.End) + .testTag(TestTag.CardWall.CAN.OrderEgkButton) + ) + } +} + +@Requirement( + "O.Data_6#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "CAN is used for eGK connection." +) +@Composable +fun CanInputField( + modifier: Modifier, + can: String, + onCanChange: (String) -> Unit, + next: () -> Unit +) { + val canRegex = """^\d{0,$CAN_LENGTH}$""".toRegex() + ErezeptOutlineText( + modifier = modifier + .testTag(TestTag.CardWall.CAN.CANField) + .fillMaxWidth(), + value = can, + onValueChange = { + if (it.matches(canRegex)) { + onCanChange(it) + } + }, + label = stringResource(R.string.can_input_field_label), + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.NumberPassword, + imeAction = ImeAction.Next + ), + shape = RoundedCornerShape(SizeDefaults.one), + keyboardActions = KeyboardActions { + if (can.length == CAN_LENGTH) { + next() + } + } + ) + SpacerTiny() + Text( + text = annotatedStringResource( + R.string.cdw_can_length_info, + CAN_LENGTH.toString() + ), + style = AppTheme.typography.caption1l + ) +} + +@LightDarkPreview +@Composable +fun CardWallCanScreenPreview(@PreviewParameter(CanPreviewParameterProvider::class) can: String) { + PreviewAppTheme { + val lazyListState = rememberLazyListState() + CanScreenContent(lazyListState, PaddingValues(PaddingDefaults.Medium), {}, {}, can, {}) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreen.kt new file mode 100644 index 00000000..d3757009 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreen.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import android.os.Build +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLargeMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString + +class CardWallGidHelpScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + val context = LocalContext.current + + BackHandler { + navController.popBackStack() + } + CardWallGidHelpScreenScaffold( + listState = listState, + onBack = { + navController.popBackStack() + }, + onClickOpenSettings = { + context.openSettingsAsNewActivity( + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> + Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS + + else -> Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS + } + ) + } + ) + } +} + +@Composable +private fun CardWallGidHelpScreenScaffold( + listState: LazyListState, + onBack: () -> Unit, + onClickOpenSettings: () -> Unit +) { + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(R.string.cardwall_gid_help_title), + onBack = onBack, + listState = listState + ) { + CardWallGidHelpScreenContent( + listState = listState, + onClickOpenSettings = onClickOpenSettings + ) + } +} + +@Composable +private fun CardWallGidHelpScreenContent( + listState: LazyListState, + onClickOpenSettings: () -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CardWallGidHelpScreenHeaderSection() + CardWallGidHelpScreenTipSection( + onClickOpenSettings = onClickOpenSettings + ) + item { SpacerXXLargeMedium() } + } +} + +@Suppress("FunctionName") +private fun LazyListScope.CardWallGidHelpScreenHeaderSection() { + item { + Column( + modifier = Modifier.padding(bottom = PaddingDefaults.Medium) + ) { + Text( + text = stringResource(R.string.cardwall_gid_help_header), + style = AppTheme.typography.h5 + ) + SpacerSmall() + Text( + text = stringResource(R.string.cardwall_gid_help_body), + style = AppTheme.typography.body2 + ) + } + } +} + +@Suppress("FunctionName") +private fun LazyListScope.CardWallGidHelpScreenTipSection( + onClickOpenSettings: () -> Unit +) { + val tips = listOf( + R.string.cardwall_gid_help_tip_1, + R.string.cardwall_gid_help_tip_2, + R.string.cardwall_gid_help_tip_3, + R.string.cardwall_gid_help_tip_4, + R.string.cardwall_gid_help_tip_5, + R.string.cardwall_gid_help_tip_6 + ) + items(tips) { + CardWallGidHelpScreenTip(stringResource(it)) + } + item { + CardWallGidHelpScreenTip( + text = stringResource(R.string.cardwall_gid_help_tip_7), + onClick = onClickOpenSettings, + buttonText = stringResource(R.string.cardwall_gid_help_settings_button).toAnnotatedString() + ) + } +} + +@Composable +private fun CardWallGidHelpScreenTip( + text: String, + onClick: () -> Unit = {}, + buttonText: AnnotatedString? = null +) { + Column(modifier = Modifier.padding(vertical = PaddingDefaults.Small)) { + Row(Modifier.fillMaxWidth()) { + Icon(Icons.Rounded.CheckCircle, null, tint = AppTheme.colors.green600) + SpacerMedium() + Text( + text = text, + style = AppTheme.typography.body1 + ) + } + buttonText?.let { + TextButton( + onClick = { onClick() } + ) { Text(text = buttonText, style = AppTheme.typography.body2) } + } + } +} + +@LightDarkPreview +@Composable +fun CardWallGidHelpScreenPreview() { + PreviewAppTheme { + CardWallGidHelpScreenScaffold( + listState = rememberLazyListState(), + onClickOpenSettings = { }, + onBack = { } + ) + } +} + +@LightDarkPreview +@Composable +fun CardWallGidHelpScreenScaffoldPreview() { + val listState = rememberLazyListState() + PreviewAppTheme { + TestScaffold( + topBarTitle = stringResource(R.string.cardwall_gid_help_title), + navigationMode = NavigationBarMode.Back + ) { + CardWallGidHelpScreenContent( + listState = listState + ) { } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreen.kt new file mode 100644 index 00000000..f18a5158 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreen.kt @@ -0,0 +1,404 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import android.app.Dialog +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.presentation.rememberExternalAuthenticatorListController +import de.gematik.ti.erp.app.cardwall.ui.components.GematikErrorDialog +import de.gematik.ti.erp.app.cardwall.ui.components.GidItem +import de.gematik.ti.erp.app.cardwall.ui.components.GidScreenHeaderSection +import de.gematik.ti.erp.app.cardwall.ui.preview.HealthInsuranceDataPreviewParameterProvider +import de.gematik.ti.erp.app.column.ColumnItems +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.idp.model.HealthInsuranceData +import de.gematik.ti.erp.app.idp.model.HealthInsuranceData.Companion.isPkv +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.LoadingDialog +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import de.gematik.ti.erp.app.utils.extensions.imeHeight +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import kotlinx.coroutines.launch + +class CardWallGidListScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val profileId by graphController.profileId.collectAsStateWithLifecycle() + val intentHandler = LocalIntentHandler.current + val dialog = LocalDialog.current + val snackbar = LocalSnackbarScaffold.current + val scope = rememberCoroutineScope() + var loadingDialog: Dialog? = remember { null } + + val listState = rememberLazyListState() + + val fastTrackClosedString = stringResource(R.string.gid_fast_track_closed_error) + + val controller = rememberExternalAuthenticatorListController() + val healthInsuranceAppIdps by controller.healthInsuranceDataList.collectAsStateWithLifecycle() + val authorizationWithExternalAppEvent = controller.authorizationWithExternalAppInBackgroundEvent + val redirectUriEvent = controller.redirectUriEvent + val redirectErrorEvent = controller.redirectUriErrorEvent + val redirectGematikErrorEvent = controller.redirectUriGematikErrorEvent + + LaunchedEffect(Unit) { + controller.getHealthInsuranceAppList() + } + + authorizationWithExternalAppEvent.listen { isStarted -> + if (isStarted) { + dialog.show { + loadingDialog = it + LoadingDialog { it.dismiss() } + } + } else { + loadingDialog?.dismiss() + } + } + + redirectUriEvent.listen { (redirectUri, healthInsuranceData) -> + intentHandler.tryStartingExternalHealthInsuranceAuthenticationApp( + redirect = redirectUri, + onSuccess = { + if (healthInsuranceData.isPkv()) { + controller.switchToPKV(profileId) + } + navController.popBackStack(CardWallRoutes.CardWallIntroScreen.route, inclusive = true) + }, + onFailure = { + dialog.show { + ErezeptAlertDialog( + title = stringResource(R.string.gid_external_app_missing_title), + body = stringResource(R.string.gid_external_app_missing_description), + okText = stringResource(R.string.ok), + onDismissRequest = { it.dismiss() } + ) + } + } + ) + } + + redirectErrorEvent.listen { + dialog.show { + ErezeptAlertDialog( + title = stringResource(R.string.main_fasttrack_error_title), + body = stringResource(R.string.main_fasttrack_error_info), + okText = stringResource(R.string.ok), + onDismissRequest = it::dismiss + ) + } + } + + redirectGematikErrorEvent.listen { responseError -> + dialog.show { + GematikErrorDialog(error = responseError) { + it.dismiss() + } + } + } + + BackHandler { + navController.navigateUp() + } + GidListScreenScaffold( + profileId = profileId, + listState = listState, + healthInsuranceAppIdps = healthInsuranceAppIdps, + filterList = controller::filterList, + unFilterList = controller::unFilterList, + reloadHealthInsuranceAppList = controller::getHealthInsuranceAppList, + startAuthorizationWithExternal = controller::startAuthorizationWithExternal, + onClickHealthInsuranceWithGidNotSupported = { + scope.launch { + snackbar.showSnackbar(fastTrackClosedString) + } + }, + onCancel = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + }, + onBack = navController::navigateUp, + onNavigateToHelp = { + navController.navigate(CardWallRoutes.CardWallGidHelpScreen.route) + } + ) + } +} + +@Composable +private fun GidListScreenScaffold( + profileId: ProfileIdentifier, + listState: LazyListState, + healthInsuranceAppIdps: UiState>, + filterList: (String) -> Unit, + unFilterList: () -> Unit, + reloadHealthInsuranceAppList: () -> Unit, + startAuthorizationWithExternal: (ProfileIdentifier, HealthInsuranceData) -> Unit, + onClickHealthInsuranceWithGidNotSupported: () -> Unit, + onCancel: () -> Unit, + onBack: () -> Unit, + onNavigateToHelp: () -> Unit +) { + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(R.string.cardwall_gid_title), + onBack = onBack, + listState = listState, + actions = { + TextButton(onClick = onCancel) { + Text(stringResource(R.string.cancel)) + } + } + ) { + GidListScreenContent( + profileId = profileId, + healthInsuranceAppIdps = healthInsuranceAppIdps, + onClickHealthInsuranceWithGidNotSupported = onClickHealthInsuranceWithGidNotSupported, + onSearch = { searchWord -> + if (searchWord.isNotEmpty()) { + filterList(searchWord) + } else { + unFilterList() + } + }, + onClickRetry = { + reloadHealthInsuranceAppList() + }, + onClickHealthInsuranceIdp = { profileId, heathInsuranceIdp -> + startAuthorizationWithExternal( + profileId, + heathInsuranceIdp + ) + }, + onNavigateToHelp = onNavigateToHelp + ) + } +} + +@Composable +fun GidListScreenContent( + profileId: ProfileIdentifier, + onSearch: (String) -> Unit, + healthInsuranceAppIdps: UiState>, + @Requirement( + "O.Auth_4#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The user selects the insurance company and the authentication process starts" + ) + onClickHealthInsuranceIdp: (ProfileIdentifier, HealthInsuranceData) -> Unit, + onClickHealthInsuranceWithGidNotSupported: () -> Unit, + onClickRetry: () -> Unit, + onNavigateToHelp: () -> Unit +) { + val scrollState = rememberScrollState() + val focusRequester = remember { FocusRequester() } + + var search by remember { mutableStateOf(TextFieldValue("")) } + + val columnArrangement by remember(healthInsuranceAppIdps) { + derivedStateOf { + when { + healthInsuranceAppIdps.isLoadingState || healthInsuranceAppIdps.isErrorState -> Arrangement.Center + else -> Arrangement.Top + } + } + } + + // column in a column to make the sticky header behaviour like a lazy column + Column { + GidScreenHeaderSection( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + searchValue = search, + focusRequester = focusRequester, + onValueChange = { + onSearch(it.text) + search = it.copy(selection = TextRange(it.text.length)) + }, + onNavigateToHelp = onNavigateToHelp + ) + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(vertical = PaddingDefaults.Medium), + verticalArrangement = columnArrangement, + horizontalAlignment = Alignment.CenterHorizontally + ) { + UiStateMachine( + state = healthInsuranceAppIdps, + onLoading = { + Box( + modifier = Modifier + .fillMaxSize() + .padding(PaddingDefaults.Large), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier + .size(PaddingDefaults.XLarge) + .align(Alignment.Center) + ) + } + }, + onError = { + ErrorScreen( + modifier = Modifier.fillMaxSize(), + onClickRetry = onClickRetry + ) + } + ) { + ColumnItems( + items = it, + itemContent = { _, healthInsuranceAppData -> + GidItem( + profileId = profileId, + healthInsuranceData = healthInsuranceAppData, + onClickHealthInsuranceIdp = onClickHealthInsuranceIdp + ) { + onClickHealthInsuranceWithGidNotSupported() + } + }, + lastItemExtraContent = { _, _ -> + Spacer( + modifier = Modifier + .size(PaddingDefaults.XXXLarge) + .imeHeight() + ) + } + ) + } + } + } +} + +@Composable +private fun ErrorScreen( + modifier: Modifier = Modifier, + onClickRetry: () -> Unit +) = + Box( + modifier = modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.align(BiasAlignment(horizontalBias = 0f, verticalBias = -0.33f)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + Text( + stringResource(R.string.cdw_fasttrack_error_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + Text( + stringResource(R.string.cdw_fasttrack_error_info), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + TextButton( + onClick = onClickRetry + ) { + Icon(Icons.Rounded.Refresh, null) + SpacerSmall() + Text(stringResource(R.string.cdw_fasttrack_try_again)) + } + } + } + +@LightDarkPreview +@Composable +fun GidListScreenScaffoldPreview( + @PreviewParameter(HealthInsuranceDataPreviewParameterProvider::class) healthInsuranceAppIds: + UiState> +) { + PreviewAppTheme { + GidListScreenContent( + profileId = "profile-id", + healthInsuranceAppIdps = healthInsuranceAppIds, + onNavigateToHelp = {}, + onClickHealthInsuranceWithGidNotSupported = {}, + onClickHealthInsuranceIdp = { _, _ -> }, + onClickRetry = {}, + onSearch = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreen.kt new file mode 100644 index 00000000..d865131b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreen.kt @@ -0,0 +1,586 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import android.app.Dialog +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.isOpaque +import androidx.compose.ui.semantics.role +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.model.GidEventData.Companion.isPkv +import de.gematik.ti.erp.app.base.fold +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.base.requireNonNull +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes.processGidEventData +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.cardwall.ui.components.EnableNfcDialog +import de.gematik.ti.erp.app.cardwall.ui.components.GematikErrorDialog +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallIntroScreenContentPreviewParameterProvider +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallIntroScreenPreviewData +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.demomode.DemoModeIntent +import de.gematik.ti.erp.app.demomode.startAppWithDemoMode +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.gid.ui.components.DomainsNotVerifiedDialog +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.semantics.semanticsButton +import de.gematik.ti.erp.app.semantics.semanticsHeading +import de.gematik.ti.erp.app.semantics.semanticsMergeDescendants +import de.gematik.ti.erp.app.semantics.semanticsMergedButton +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXLarge +import de.gematik.ti.erp.app.utils.SpacerXXXLarge +import de.gematik.ti.erp.app.utils.compose.ClickText +import de.gematik.ti.erp.app.utils.compose.ClickableText +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.HintTextActionButton +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.LoadingDialog +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog + +@Requirement( + "O.Resi_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Selection of login methods for the user" +) +class CardWallIntroScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + requireNonNull( + navBackStackEntry.arguments?.getString( + CardWallRoutes.CARD_WALL_NAV_PROFILE_ID + ) + ).fold( + onSuccess = { profileId -> + // this information is available on the screen only when the process wants gid authentication + val gidEventData = navBackStackEntry.processGidEventData() + + var loadingDialog: Dialog? = remember { null } + + graphController.setProfileId(profileId) + + val lazyListState = rememberLazyListState() + val cardWallController = rememberCardWallController() + + val activity = LocalActivity.current + val dialog = LocalDialog.current + val context = LocalContext.current + val intentHandler = LocalIntentHandler.current + + val nfcDisabledEvent = ComposableEvent() + val domainsAreNotVerifiedEvent = ComposableEvent() + + val isDomainVerified = cardWallController.isDomainVerified + val isNfcAvailable by cardWallController.isNFCAvailable + + with(graphController) { + authorizationWithExternalAppInBackgroundEvent.listen { isStarted -> + if (isStarted) { + dialog.show { + loadingDialog = it + LoadingDialog { it.dismiss() } + } + } else { + loadingDialog?.dismiss() + } + } + redirectUriEvent.listen { (redirectUri, gidEventData) -> + intentHandler.tryStartingExternalHealthInsuranceAuthenticationApp( + redirect = redirectUri, + onSuccess = { + if (gidEventData.isPkv()) { + graphController.switchToPKV(profileId) + } + navController.popBackStack() + }, + onFailure = { + dialog.show { + ErezeptAlertDialog( + title = stringResource(R.string.gid_external_app_missing_title), + body = stringResource(R.string.gid_external_app_missing_description), + okText = stringResource(R.string.ok), + onDismissRequest = { it.dismiss() } + ) + } + } + ) + } + redirectUriErrorEvent.listen { + dialog.show { + ErezeptAlertDialog( + title = stringResource(R.string.main_fasttrack_error_title), + body = stringResource(R.string.main_fasttrack_error_info), + okText = stringResource(R.string.ok), + onDismissRequest = it::dismiss + ) + } + } + redirectUriGematikErrorEvent.listen { responseError -> + dialog.show { + GematikErrorDialog(error = responseError) { + it.dismiss() + } + } + } + } + + nfcDisabledEvent.listen { + dialog.show { + EnableNfcDialog( + onClickAction = { it.dismiss() }, + onCancel = { it.dismiss() } + ) + } + } + + domainsAreNotVerifiedEvent.listen { + dialog.show { + DomainsNotVerifiedDialog( + onClickSettingsOpen = { + context.openSettingsAsNewActivity(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + it.dismiss() + }, + onDismissRequest = { it.dismiss() } + ) + } + } + + BackHandler { + navController.popBackStack() + } + CardWallScaffold( + modifier = Modifier + .testTag(TestTag.CardWall.Intro.IntroScreen) + .systemBarsPadding(), + listState = lazyListState, + title = "", + backMode = null, + onNext = null, + nextText = "", + actions = { + TextButton( + onClick = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + } + ) { + Text(stringResource(R.string.cancel)) + } + }, + onBack = { navController.popBackStack() } + ) { + CardWallIntroScreenContent( + isNfcAvailable = isNfcAvailable, + isNfcEnabled = cardWallController.isNfcEnabled, + isDomainVerified = isDomainVerified, + listState = lazyListState, + showEnableNFCDialog = { nfcDisabledEvent.trigger(Unit) }, + insuranceName = gidEventData?.authenticatorName, + onClickOrderNow = { + navController.navigate( + OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.path() + ) + }, + onClickHealthCardAuth = { navController.navigate(CardWallRoutes.CardWallCanScreen.path()) }, + onClickInsuranceAuth = { + gidEventData?.let { + graphController.startAuthorizationWithExternal(gidEventData) + } ?: navController.navigate(CardWallRoutes.CardWallGidListScreen.path()) + }, + showVerifyDomainDialog = { domainsAreNotVerifiedEvent.trigger() }, + onClickDemoMode = { + DemoModeIntent.startAppWithDemoMode(activity = activity) + } + ) + } + } + ) + } +} + +@Requirement( + "A_25416#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Selection of login methods for the user", + codeLines = 36 +) +@Composable +private fun CardWallIntroScreenContent( + isNfcAvailable: Boolean, + isDomainVerified: Boolean, + listState: LazyListState, + isNfcEnabled: Boolean, + insuranceName: String?, + showEnableNFCDialog: () -> Unit, + onClickHealthCardAuth: () -> Unit, + onClickInsuranceAuth: () -> Unit, + onClickOrderNow: () -> Unit, + showVerifyDomainDialog: () -> Unit, + onClickDemoMode: () -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + HealthCardPhoneImage() + Header() + SubTitleHeader() + HealthCardLoginSection( + isNfcAvailable = isNfcAvailable, + isNfcEnabled = isNfcEnabled, + onClickWithEnabledNfc = onClickHealthCardAuth, + onClickWithDisabledNfc = showEnableNFCDialog + ) + @Requirement( + "O.Auth_4#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The UI component that allows the user to login via GID" + ) + GidLoginSection( + isDomainVerified = isDomainVerified, + insuranceName = insuranceName, + showVerifyDomainDialog = showVerifyDomainDialog, + onClick = onClickInsuranceAuth + ) + OrderHealthCardHintSection(onClickOrderNow) + TryDemomodeSection(onClickDemoMode) + } +} + +@Suppress("FunctionName") +private fun LazyListScope.HealthCardPhoneImage() { + item { + Image( + painterResource(R.drawable.card_wall_card_hand), + contentDescription = stringResource(R.string.a11y_card_wall_card_hand), + contentScale = ContentScale.FillWidth, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) + } +} + +@Suppress("FunctionName") +private fun LazyListScope.Header() { + item { + Text( + stringResource(R.string.cdw_intro_header), + style = AppTheme.typography.h5, + color = AppTheme.colors.neutral900, + modifier = Modifier.testTag("cdw_txt_intro_header_bottom") + ) + } +} + +@Suppress("FunctionName") +private fun LazyListScope.SubTitleHeader() { + item { SpacerSmall() } + item { + Text( + stringResource(R.string.cdw_intro_info), + style = AppTheme.typography.subtitle2, + textAlign = TextAlign.Center, + color = AppTheme.colors.neutral600 + ) + } + item { SpacerXLarge() } +} + +@Suppress("FunctionName") +@OptIn(ExperimentalMaterialApi::class) +private fun LazyListScope.HealthCardLoginSection( + isNfcAvailable: Boolean, + isNfcEnabled: Boolean, + onClickWithEnabledNfc: () -> Unit, + onClickWithDisabledNfc: () -> Unit +) { + item { + Column( + modifier = Modifier.fillMaxWidth() + ) { + if (isNfcAvailable) { + Text( + text = stringResource(R.string.cdw_intro_auth_prior), + modifier = Modifier + .align(Alignment.Start) + .offset(x = PaddingDefaults.Medium) + .semanticsHeading(), + style = AppTheme.typography.subtitle2, + fontWeight = FontWeight.Bold, + color = AppTheme.colors.primary600 + ) + } + Card( + modifier = Modifier + .padding(bottom = PaddingDefaults.Medium) + .fillMaxWidth(), + shape = RoundedCornerShape(SizeDefaults.double), + border = if (isNfcAvailable) BorderStroke(SizeDefaults.quarter, color = AppTheme.colors.primary600) else null, + elevation = SizeDefaults.zero, + backgroundColor = AppTheme.colors.neutral050, + enabled = isNfcAvailable, + onClick = { + if (isNfcEnabled) { + onClickWithEnabledNfc() + } else { + onClickWithDisabledNfc() + } + } + ) { + Row( + modifier = Modifier.padding(PaddingDefaults.Medium) + ) { + Column( + modifier = Modifier + .weight(1f) + .semanticsMergeDescendants { + if (isNfcAvailable) { + role = Role.Button + } else { + disabled() + isOpaque() + } + } + ) { + Text( + stringResource(R.string.cdw_intro_auth_health_card), + style = AppTheme.typography.subtitle1l, + color = when { + isNfcAvailable -> AppTheme.colors.neutral900 + else -> AppTheme.colors.neutral400 + } + ) + SpacerTiny() + Text( + when { + isNfcAvailable -> stringResource(R.string.cdw_intro_auth_health_card_pin) + else -> stringResource(R.string.cdw_health_card_no_nfc_device) + }, + style = AppTheme.typography.body2l, + color = when { + isNfcAvailable -> AppTheme.colors.neutral600 + else -> AppTheme.colors.neutral400 + } + ) + } + Icon( + modifier = Modifier + .size(SizeDefaults.triple) + .align(Alignment.CenterVertically), + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = if (isNfcAvailable) AppTheme.colors.primary600 else AppTheme.colors.neutral300 + ) + } + } + } + } +} + +@Suppress("FunctionName") +@OptIn(ExperimentalMaterialApi::class) +private fun LazyListScope.GidLoginSection( + isDomainVerified: Boolean, + insuranceName: String?, + showVerifyDomainDialog: () -> Unit, + onClick: () -> Unit +) { + item { + Card( + modifier = Modifier + .padding(bottom = PaddingDefaults.Medium) + .fillMaxWidth(), + shape = RoundedCornerShape(SizeDefaults.double), + border = BorderStroke(SizeDefaults.eighth, color = AppTheme.colors.neutral300), + elevation = SizeDefaults.zero, + backgroundColor = AppTheme.colors.neutral050, + onClick = { + when { + isDomainVerified -> onClick() + else -> showVerifyDomainDialog() + } + } + ) { + Row( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .semanticsMergedButton(), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + stringResource(R.string.cdw_intro_auth_gid), + style = AppTheme.typography.subtitle1l, + color = AppTheme.colors.neutral900 + ) + SpacerTiny() + Text( + text = if (insuranceName != null) { + stringResource(R.string.cdw_intro_auth_gid_app_required, insuranceName) + } else { + stringResource(R.string.cdw_intro_auth_additional_app_required) + }, + style = AppTheme.typography.body2l, + color = AppTheme.colors.neutral600 + ) + } + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + null, + tint = AppTheme.colors.neutral400, + modifier = Modifier + .size(SizeDefaults.triple) + .align(Alignment.CenterVertically) + ) + } + } + } +} + +@Suppress("FunctionName") +private fun LazyListScope.OrderHealthCardHintSection( + onClickOrderNow: () -> Unit +) { + item { SpacerSmall() } + item { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.cdw_have_no_card_with_pin), + style = AppTheme.typography.body2l + ) + HintTextActionButton( + text = stringResource(R.string.cdw_intro_order_now), + align = Alignment.End, + modifier = Modifier + .semanticsButton() + .align(Alignment.End) + .testTag(TestTag.CardWall.Intro.OrderEgkButton) + ) { + onClickOrderNow() + } + } + } +} + +@Suppress("FunctionName") +private fun LazyListScope.TryDemomodeSection(onClickDemoMode: () -> Unit) { + item { + Column( + modifier = Modifier.semanticsMergedButton() + ) { + SpacerMedium() + ClickableText( + modifier = Modifier.padding(horizontal = PaddingDefaults.Large), + textWithPlaceholdersRes = R.string.demo_mode_start_text, + textStyle = AppTheme.typography.body1l, + clickText = ClickText( + text = stringResource(R.string.demo_mode_link_text), + onClick = onClickDemoMode + ) + ) + SpacerXXXLarge() + } + } +} + +@LightDarkPreview +@Composable +fun CardWallIntroScreenContentPreview( + @PreviewParameter(CardWallIntroScreenContentPreviewParameterProvider::class) state: + CardWallIntroScreenPreviewData +) { + PreviewAppTheme { + CardWallIntroScreenContent( + listState = rememberLazyListState(), + isNfcAvailable = state.isNfcAvailable, + isNfcEnabled = state.isNfcEnabled, + isDomainVerified = state.isDomainVerified, + insuranceName = null, + showEnableNFCDialog = { }, + onClickHealthCardAuth = { }, + onClickInsuranceAuth = { }, + onClickOrderNow = { }, + showVerifyDomainDialog = { }, + onClickDemoMode = { } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreen.kt new file mode 100644 index 00000000..bb5860c1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreen.kt @@ -0,0 +1,357 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.DisableSelection +import androidx.compose.material.Icon +import androidx.compose.material.IconToggleButton +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.presentation.deviceDeviceSecurityStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceHardwareBackedKeystoreStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceSupportsAuthenticationMethod +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallPinScreenPreviewData +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallPinScreenPreviewParameterProvider +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus +import de.gematik.ti.erp.app.utils.extensions.disableCopyPasteFromKeyboard +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty +import de.gematik.ti.erp.app.utils.letNotNullOnCondition + +val PIN_RANGE = 6..8 + +@Requirement( + "O.Data_6#8", + sourceSpecification = "BSI-eRp-ePA", + rationale = ".. the screen where PIN is entered from." +) +@Requirement( + "O.Purp_2#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The Pin is used to verify that the user is the owner of the eGK. (has the knowledge of the PIN)" +) +class CardWallPinScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val canNumber = navBackStackEntry.arguments?.getString(CardWallRoutes.CARD_WALL_NAV_CAN) + val profileId = navBackStackEntry.arguments?.getString(CardWallRoutes.CARD_WALL_PIN_NAV_PROFILE_ID) + + // set the CAN and profileId in the graphController when its starting this screen from the navigation + letNotNullOnCondition( + first = canNumber, + second = profileId, + condition = { + canNumber.isNotNullOrEmpty() && profileId.isNotNullOrEmpty() + } + ) { can, id -> + graphController.setCardAccessNumber(can) + graphController.setProfileId(id) + } + + val pin by graphController.pin.collectAsStateWithLifecycle() + val lazyListState = rememberLazyListState() + val context = LocalContext.current + val biometricState = remember { context.deviceDeviceSecurityStatus() } + val onNext = { + val deviceSupportsBiometrics = deviceSupportsAuthenticationMethod(biometricState) + + if (context.deviceHardwareBackedKeystoreStatus() && deviceSupportsBiometrics) { + navController.navigate(CardWallRoutes.CardWallSaveCredentialsScreen.path()) + } else { + navController.navigate(CardWallRoutes.CardWallReadCardScreen.path()) + } + } + val onBack: () -> Unit = { + graphController.resetPin() + navController.popBackStack() + } + val onExit: () -> Unit = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + } + BackHandler { onBack() } + + CardWallScaffold( + modifier = Modifier.testTag(TestTag.CardWall.PIN.PinScreen), + onBack = onBack, + title = stringResource(R.string.cdw_top_bar_title), + nextEnabled = pin.length in PIN_RANGE, + onNext = onNext, + listState = lazyListState, + nextText = stringResource(R.string.unlock_egk_next), + actions = { + TextButton( + onClick = onExit + ) { + Text(stringResource(R.string.cancel)) + } + } + ) { innerPadding -> + CardWallPinScreenContent( + lazyListState = lazyListState, + innerPadding = innerPadding, + pin = pin, + onNext = onNext, + onPinChange = { graphController.setPersonalIdentificationNumber(it) }, + onClickNoPinReceived = { + navController.navigate(OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.path()) + } + ) + } + } +} + +@Requirement( + "O.Data_6#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "PIN is used for eGK connection." +) +@Composable +private fun CardWallPinScreenContent( + lazyListState: LazyListState, + innerPadding: PaddingValues, + onNext: () -> Unit, + pin: String, + onPinChange: (String) -> Unit, + onClickNoPinReceived: () -> Unit +) { + val contentPadding by rememberContentPadding(innerPadding) + + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding + ) { + item { + Text( + stringResource(R.string.cdw_pin_title), + style = AppTheme.typography.h5 + ) + SpacerSmall() + } + item { + Text( + stringResource(R.string.cdw_pin_info), + style = AppTheme.typography.body1 + ) + SpacerSmall() + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.CardWall.PIN.OrderEgkButton), + contentAlignment = Alignment.CenterEnd + ) { + ClickableTaggedText( + text = annotatedLinkStringLight( + uri = "", + text = stringResource(R.string.cdw_no_pin_received) + ), + onClick = { onClickNoPinReceived() }, + style = AppTheme.typography.body2 + ) + } + SpacerXXLarge() + } + item { + PinInputField( + modifier = Modifier + .fillMaxWidth() + .scrollOnFocus(to = 3, lazyListState), + pinRange = PIN_RANGE, + onPinChange = onPinChange, + pin = pin, + onNext = onNext, + infoText = annotatedStringResource( + R.string.cdw_pin_length_info, + PIN_RANGE.first.toString(), + PIN_RANGE.last.toString() + ).text + ) + } + } +} + +@Requirement( + "O.Data_11#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Password fields using the keyboard type numberPassword. " + + "Autocorrect is disallowed. It`s not possible to disable third party keyboards." +) +@Requirement( + "O.Data_10#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "..using the autofill disabled keyboard options for pin entry.", + codeLines = 30 +) +@Composable +fun PinInputField( + modifier: Modifier, + pinRange: IntRange, + onPinChange: (String) -> Unit, + pin: String, + label: String = stringResource(R.string.cdw_pin_label), + isConsistent: Boolean = true, + infoText: String, + onNext: () -> Unit +) { + val secretRegexString = "^\\d{0,${pinRange.last}}$" + val secretRegex = secretRegexString.toRegex() + var secretVisible by remember { mutableStateOf(false) } + DisableSelection { + ErezeptOutlineText( + modifier = modifier.disableCopyPasteFromKeyboard(), + value = pin, + onValueChange = { + if (it.matches(secretRegex)) { + onPinChange(it) + } + }, + label = label, + visualTransformation = if (secretVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + keyboardOptions = autofillDisabledNumberKeyboardOptions(), + shape = RoundedCornerShape(SizeDefaults.one), + keyboardActions = KeyboardActions { + if (isConsistent && pin.length in pinRange) { + onNext() + } + }, + trailingIcon = { + IconToggleButton( + checked = secretVisible, + onCheckedChange = { secretVisible = it } + ) { + Icon( + if (secretVisible) { + Icons.Rounded.Visibility + } else { + Icons.Rounded.VisibilityOff + }, + null + ) + } + } + ) + } + SpacerTiny() + Text( + text = infoText, + style = AppTheme.typography.caption1l + ) +} + +@Requirement( + "O.Data_10#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "By disabling autofill we eliminate any recordings that can be done while typing the keyboard." +) +private fun autofillDisabledNumberKeyboardOptions() = + KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.NumberPassword, + imeAction = ImeAction.Next + ) + +@LightDarkPreview +@Composable +fun CardWallPinScreenPreview( + @PreviewParameter( + CardWallPinScreenPreviewParameterProvider::class + ) previewData: CardWallPinScreenPreviewData +) { + PreviewAppTheme { + CardWallScaffold( + onBack = { }, + title = stringResource(R.string.cdw_top_bar_title), + nextEnabled = previewData.pin.length in PIN_RANGE, + onNext = { }, + listState = rememberLazyListState(), + nextText = stringResource(R.string.unlock_egk_next) + ) { innerPadding -> + CardWallPinScreenContent( + lazyListState = rememberLazyListState(), + innerPadding = innerPadding, + pin = previewData.pin, + onNext = { }, + onPinChange = { }, + onClickNoPinReceived = { } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreen.kt new file mode 100644 index 00000000..4e09d2d3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreen.kt @@ -0,0 +1,167 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.presentation.SaveCredentialsController +import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallController +import de.gematik.ti.erp.app.cardwall.presentation.rememberCardWallNfcPositionState +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallAuthenticationData +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallAuthenticationDialog +import de.gematik.ti.erp.app.cardwall.ui.components.EnableNfcDialog +import de.gematik.ti.erp.app.cardwall.ui.components.ReadCardScreenScaffold +import de.gematik.ti.erp.app.cardwall.ui.components.rememberCardWallAuthenticationDialogState +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreview +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreviewParameter +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class CardWallReadCardScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val cardWallController = rememberCardWallController() + val dialogState = rememberCardWallAuthenticationDialogState() + + val nfcPositionState = rememberCardWallNfcPositionState() + val nfcPos = nfcPositionState.state.nfcData.nfcPos + + val profileId by graphController.profileId.collectAsStateWithLifecycle() + val saveCredentials by graphController.saveCredentials.collectAsStateWithLifecycle() + val can by graphController.can.collectAsStateWithLifecycle() + val pin by graphController.pin.collectAsStateWithLifecycle() + + val authenticationData by remember(saveCredentials) { + derivedStateOf { + (saveCredentials as? SaveCredentialsController.AuthResult.Initialized)?.let { + CardWallAuthenticationData.SaveCredentialsWithHealthCard( + cardAccessNumber = can, + personalIdentificationNumber = pin, + initialPairingData = it + ) + } ?: CardWallAuthenticationData.HealthCard( + cardAccessNumber = can, + personalIdentificationNumber = pin + ) + } + } + + BackHandler { + navController.navigateUp() + } + ReadCardScreenScaffold( + onBack = navController::navigateUp, + onClickTroubleshooting = { + navController.navigate( + TroubleShootingRoutes.TroubleShootingIntroScreen.path() + ) + }, + nfcPosition = nfcPos + ) + if (!cardWallController.isNfcEnabled) { + EnableNfcDialog { + navController.popBackStack() + } + } else { + CardWallAuthenticationDialog( + dialogState = dialogState, + cardWallController = cardWallController, + authenticationData = authenticationData, + profileId = profileId, + troubleShootingEnabled = true, + allowUserCancellation = true, + onFinal = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + }, + onUnlockEgk = { + graphController.reset() + navController.navigate( + CardUnlockRoutes.CardUnlockIntroScreen.path(unlockMethod = UnlockMethod.ResetRetryCounter.name), + navOptions = navOptions { + popUpTo(CardWallRoutes.CardWallIntroScreen.route) { + inclusive = true + } + } + ) + }, + onRetryCan = { + navController.navigate( + CardWallRoutes.CardWallCanScreen.path(), + navOptions = navOptions { + popUpTo(CardWallRoutes.CardWallCanScreen.route) { + inclusive = true + } + } + ) + }, + onRetryPin = { + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + can = can, + profileIdentifier = profileId + ), + navOptions = navOptions { + popUpTo(CardWallRoutes.CardWallPinScreen.route) { + inclusive = true + } + } + ) + }, + onClickTroubleshooting = { + navController.navigate( + TroubleShootingRoutes.TroubleShootingIntroScreen.path() + ) + } + ) + } + } +} + +@LightDarkPreview +@Composable +fun CardWallReadCardScreenPreview( + @PreviewParameter(NfcPositionPreviewParameter::class) previewData: NfcPositionPreview +) { + PreviewAppTheme { + ReadCardScreenScaffold( + onBack = {}, + onClickTroubleshooting = {}, + nfcPosition = previewData.nfcPos + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsInfoScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsInfoScreen.kt new file mode 100644 index 00000000..634e54e6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsInfoScreen.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import android.os.Build +import androidx.activity.compose.BackHandler +import androidx.biometric.BiometricManager +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.presentation.deviceStrongBoxStatus +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.presentation.SaveCredentialsController +import de.gematik.ti.erp.app.cardwall.presentation.rememberSaveCredentialsScreenController +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import kotlinx.coroutines.launch + +@Requirement( + "O.Resi_1#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "User information and acceptance/deny for saving credentials" +) +class CardWallSaveCredentialsInfoScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val saveCredentialsController = rememberSaveCredentialsScreenController() + val scope = rememberCoroutineScope() + val listState = rememberLazyListState() + val saveCredentials by graphController.saveCredentials.collectAsStateWithLifecycle() + val context = LocalContext.current + val activity = LocalActivity.current + val biometricManager = BiometricManager.from(activity) + val useStrongBox by remember { mutableStateOf(context.deviceStrongBoxStatus()) } + val biometricStrong = remember { + biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == + BiometricManager.BIOMETRIC_SUCCESS + } + LaunchedEffect(Unit) { + (saveCredentials as? SaveCredentialsController.AuthResult.Initialized)?.let { + saveCredentialsController.cleanup(it.aliasOfSecureElementEntry) + graphController.setSaveCredentials(null) + } + } + BackHandler { + navController.popBackStack() + } + CardWallSaveCredentialsInfoScreenScaffold( + onBack = { + navController.popBackStack() + }, + onAccept = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + scope.launch { + when (val r = saveCredentialsController.initializeAndPrompt(useStrongBox = useStrongBox)) { + is SaveCredentialsController.AuthResult.Initialized -> { + graphController.setSaveCredentials(r) + navController.navigate( + CardWallRoutes.CardWallReadCardScreen.path(), + navOptions = navOptions { + popUpTo(CardWallRoutes.CardWallSaveCredentialsScreen.route) + } + ) + } + else -> { + navController.popBackStack() + } + } + } + } else { + navController.popBackStack() + } + }, + listState = listState + ) { innerPadding -> + CardWallSaveCredentialsInfoScreenContent( + listState = listState, + biometricStrong = biometricStrong, + innerPadding = innerPadding + ) + } + } +} + +@Composable +private fun CardWallSaveCredentialsInfoScreenContent( + listState: LazyListState, + biometricStrong: Boolean, + innerPadding: PaddingValues +) { + val contentPadding by rememberContentPadding(innerPadding) + + LazyColumn( + contentPadding = contentPadding, + state = listState + ) { + item { + Text( + stringResource(R.string.cdw_info_header), + style = AppTheme.typography.h5 + ) + SpacerSmall() + } + item { + Text( + if (biometricStrong) { stringResource(R.string.cdw_info_first) } else { + stringResource(R.string.cdw_info_no_strong_biometry_first) + }, + style = AppTheme.typography.body1 + ) + SpacerSmall() + } + item { + Text( + if (biometricStrong) { stringResource(R.string.cdw_info_second) } else { + stringResource(R.string.cdw_info_no_strong_biometry_second) + }, + style = AppTheme.typography.body1 + ) + SpacerSmall() + } + item { + Text( + if (biometricStrong) { stringResource(R.string.cdw_info_third) } else { + stringResource(R.string.cdw_info_no_strong_biometry_third) + }, + style = AppTheme.typography.body1 + ) + SpacerXXLarge() + } + } +} + +@Composable +fun CardWallSaveCredentialsInfoScreenScaffold( + listState: LazyListState, + onAccept: () -> Unit, + onBack: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.systemBarsPadding(), + topBarTitle = stringResource(R.string.cdw_info_title), + topBarColor = MaterialTheme.colors.background, + listState = listState, + navigationMode = NavigationBarMode.Close, + bottomBar = { + CardWallSaveCredentialsInfoScreenBottomBar( + onNext = onAccept + ) + }, + onBack = onBack, + content = content + ) +} + +@Composable +private fun CardWallSaveCredentialsInfoScreenBottomBar( + onNext: () -> Unit +) { + Surface( + color = MaterialTheme.colors.surface, + elevation = SizeDefaults.half + ) { + Column( + Modifier + .navigationBarsPadding() + .fillMaxWidth() + ) { + PrimaryButton( + onClick = onNext, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.CardWall.SecurityAcceptance.AcceptButton) + .padding( + horizontal = SizeDefaults.ninefold, + vertical = PaddingDefaults.ShortMedium + ) + ) { + Text( + stringResource(R.string.cdw_info_accept) + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreen.kt new file mode 100644 index 00000000..2a25ae47 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreen.kt @@ -0,0 +1,330 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import android.content.Context +import androidx.biometric.BiometricManager +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.RadioButtonUnchecked +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.presentation.deviceDeviceSecurityStatus +import de.gematik.ti.erp.app.authentication.ui.components.EnrollBiometricDialog +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.cardwall.navigation.CardWallScreen +import de.gematik.ti.erp.app.cardwall.presentation.CardWallGraphController +import de.gematik.ti.erp.app.cardwall.ui.components.CardWallScaffold +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallSaveCredentialsPreviewParameterProvider +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallSaveCredentialsScreenPreviewData +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.HintCard +import de.gematik.ti.erp.app.utils.compose.HintSmallImage +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.extensions.LocalDialog + +enum class AuthenticationMethod { + None, + Alternative, // e.g. biometrics + HealthCard +} + +class CardWallSaveCredentialsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: CardWallGraphController +) : CardWallScreen() { + @Composable + override fun Content() { + val context = LocalContext.current + val dialog = LocalDialog.current + val showEnrollBiometricEvent = ComposableEvent() + var selectedAuthMode by remember { mutableStateOf(AuthenticationMethod.None) } + var showFutureLogOutHint by remember { mutableStateOf(false) } + val lazyListState = rememberLazyListState() + CardWallScaffold( + modifier = Modifier + .testTag("cardWall/authenticationSelection"), + title = stringResource(R.string.cdw_top_bar_title), + nextEnabled = selectedAuthMode != AuthenticationMethod.None, + onNext = { + navController.navigate( + CardWallRoutes.CardWallReadCardScreen.path() + ) + }, + onBack = { + navController.popBackStack() + }, + listState = lazyListState, + nextText = stringResource(R.string.cdw_next), + actions = { + TextButton(onClick = { + graphController.reset() + navController.popBackStack(CardWallRoutes.subGraphName(), inclusive = true) + }) { + Text(stringResource(R.string.cancel)) + } + } + ) { innerPadding -> + @Requirement( + "O.Resi_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Selection of secure save credentials opens a info screen with more details" + ) + CardWallSaveCredentialsScreenContent( + listState = lazyListState, + innerPadding = innerPadding, + selectedAuthMode = selectedAuthMode, + context = context, + showFutureLogOutHint = showFutureLogOutHint, + onShowBiometricDialog = { showEnrollBiometricEvent.trigger(Unit) }, + onShowFutureLogOutHint = { showFutureLogOutHint = it }, + onSelectAlternativeOption = { selectedAuthMode = it }, + onOpenInfoScreen = { + navController.navigate( + CardWallRoutes.CardWallSaveCredentialsInfoScreen.path() + ) + } + ) + } + EnrollBiometricDialog( + context = context, + dialog = dialog, + event = showEnrollBiometricEvent + ) + } +} + +@Composable +private fun CardWallSaveCredentialsScreenContent( + listState: LazyListState, + innerPadding: PaddingValues, + selectedAuthMode: AuthenticationMethod, + context: Context, + showFutureLogOutHint: Boolean, + onSelectAlternativeOption: (AuthenticationMethod) -> Unit, + onShowBiometricDialog: () -> Unit, + onShowFutureLogOutHint: (Boolean) -> Unit, + onOpenInfoScreen: () -> Unit +) { + val contentPadding by rememberContentPadding(innerPadding) + + LazyColumn( + state = listState, + contentPadding = contentPadding + ) { + item { + Text( + stringResource(R.string.cdw_selection_title), + style = AppTheme.typography.h5, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) + SpacerXXLarge() + } + item { + SelectableCard( + modifier = Modifier.testTag(TestTag.CardWall.StoreCredentials.Save), + selected = selectedAuthMode == AuthenticationMethod.Alternative, + startIcon = Icons.Rounded.Check, + text = stringResource(R.string.cdw_selection_save) + ) { + onShowFutureLogOutHint(false) + if (context.deviceDeviceSecurityStatus() == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + onShowBiometricDialog() + } else { + onSelectAlternativeOption(AuthenticationMethod.Alternative) + onOpenInfoScreen() + } + } + SpacerMedium() + } + item { + SelectableCard( + modifier = Modifier + .testTag(TestTag.CardWall.StoreCredentials.DontSave), + selected = selectedAuthMode == AuthenticationMethod.HealthCard, + startIcon = Icons.Rounded.Close, + text = stringResource(R.string.cdw_selection_save_not) + ) { + onSelectAlternativeOption(AuthenticationMethod.HealthCard) + onShowFutureLogOutHint(true) + } + SpacerXXLarge() + } + item { + if (showFutureLogOutHint) { + SpacerMedium() + HintCard( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + image = { + HintSmallImage(painterResource(R.drawable.information), null, it) + }, + title = { Text(stringResource(R.string.cdw_selection_hint_title)) }, + body = { Text(stringResource(R.string.cdw_selection_hint_info_)) } + ) + SpacerXXLarge() + } + } + } +} + +@Composable +private fun SelectableCard( + modifier: Modifier = Modifier, + selected: Boolean = false, + startIcon: ImageVector, + text: String, + onCardSelected: () -> Unit = {} +) { + val checkIcon = if (selected) { + Icons.Rounded.CheckCircle + } else { + Icons.Rounded.RadioButtonUnchecked + } + + val checkIconTint = if (selected) { + AppTheme.colors.primary600 + } else { + AppTheme.colors.neutral400 + } + + val cardBorderStroke = if (selected) { + BorderStroke(SizeDefaults.quarter, AppTheme.colors.primary600) + } else { + BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral300) + } + + Card( + border = cardBorderStroke, + backgroundColor = AppTheme.colors.neutral000, + modifier = modifier + .padding(horizontal = PaddingDefaults.Medium) + .fillMaxWidth(), + shape = RoundedCornerShape(SizeDefaults.one) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + enabled = true, + onClick = onCardSelected + ) + .padding(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + startIcon, + null, + tint = AppTheme.colors.primary600 + ) + + Text( + text, + style = AppTheme.typography.subtitle1, + modifier = modifier + .weight(1f) + .padding(start = PaddingDefaults.Medium) + ) + + Icon( + checkIcon, + null, + tint = checkIconTint + ) + } + } +} + +@LightDarkPreview +@Composable +fun CardWallSaveCredentialsScreenPreview( + @PreviewParameter(CardWallSaveCredentialsPreviewParameterProvider::class) previewData: + CardWallSaveCredentialsScreenPreviewData +) { + val lazyListState = rememberLazyListState() + val sampleContext = LocalContext.current + + var selectedAuthMode by remember { mutableStateOf(previewData.selectedAuthMode) } + var showFutureLogOutHint by remember { mutableStateOf(previewData.showFutureLogOutHint) } + + PreviewAppTheme { + CardWallScaffold( + title = stringResource(R.string.cdw_top_bar_title), + onBack = { }, + onNext = {}, + nextEnabled = selectedAuthMode != AuthenticationMethod.None, + nextText = stringResource(R.string.cdw_next), + backMode = NavigationBarMode.Back, + listState = lazyListState, + content = { innerPadding -> + CardWallSaveCredentialsScreenContent( + listState = lazyListState, + innerPadding = innerPadding, + selectedAuthMode = selectedAuthMode, + context = sampleContext, + showFutureLogOutHint = showFutureLogOutHint, + onShowBiometricDialog = {}, + onShowFutureLogOutHint = { showFutureLogOutHint = it }, + onSelectAlternativeOption = { selectedAuthMode = it }, + onOpenInfoScreen = { } + ) + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt index fa66f2e1..23675e71 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.usecase @@ -27,19 +27,19 @@ import androidx.compose.runtime.Stable import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.api.ApiCallException import de.gematik.ti.erp.app.card.model.command.ResponseException -import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcCardChannel -import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcCardSecureChannel import de.gematik.ti.erp.app.card.model.command.ResponseStatus -import de.gematik.ti.erp.app.cardwall.model.nfc.exchange.establishTrustedChannel import de.gematik.ti.erp.app.card.model.exchange.retrieveCertificate import de.gematik.ti.erp.app.card.model.exchange.signChallenge import de.gematik.ti.erp.app.card.model.exchange.verifyPin +import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcCardChannel +import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcCardSecureChannel +import de.gematik.ti.erp.app.cardwall.model.nfc.exchange.establishTrustedChannel import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.idp.usecase.AltAuthenticationCryptoException import de.gematik.ti.erp.app.idp.usecase.IdpUseCase import de.gematik.ti.erp.app.profiles.repository.KVNRAlreadyAssignedException import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.io.IOException +import io.github.aakira.napier.Napier import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ProducerScope @@ -54,7 +54,7 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import org.json.JSONException import org.json.JSONObject -import io.github.aakira.napier.Napier +import java.io.IOException import java.security.PublicKey @Stable @@ -78,19 +78,25 @@ sealed class AuthenticationState { object HealthCardCommunicationInterrupted : AuthenticationState() // health card failure states - object HealthCardCardAccessNumberWrong : AuthenticationState() - object HealthCardPin2RetriesLeft : AuthenticationState() - object HealthCardPin1RetryLeft : AuthenticationState() - object HealthCardBlocked : AuthenticationState() + data object HealthCardCardAccessNumberWrong : AuthenticationState() + data object HealthCardPin2RetriesLeft : AuthenticationState() + data object HealthCardPin1RetryLeft : AuthenticationState() + data object HealthCardBlocked : AuthenticationState() + @Requirement( + "A_20605#5", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "The errors are mapped to their descriptive localized errors to be shown in the application.", + codeLines = 10 + ) // IDP failure states - object IDPCommunicationFailed : AuthenticationState() + data object IDPCommunicationFailed : AuthenticationState() - object UserNotAuthenticated : AuthenticationState() + data object UserNotAuthenticated : AuthenticationState() - object IDPCommunicationAltAuthNotSuccessful : AuthenticationState() - object IDPCommunicationInvalidCertificate : AuthenticationState() - object IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate : AuthenticationState() + data object IDPCommunicationAltAuthNotSuccessful : AuthenticationState() + data object IDPCommunicationInvalidCertificate : AuthenticationState() + data object IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate : AuthenticationState() // profile failure class InsuranceIdentifierAlreadyExists( @@ -117,6 +123,7 @@ sealed class AuthenticationState { UserNotAuthenticated, is InsuranceIdentifierAlreadyExists, SecureElementCryptographyFailed -> true + else -> false } @@ -132,6 +139,7 @@ sealed class AuthenticationState { HealthCardCommunicationCertificateLoaded, HealthCardCommunicationFinished, IDPCommunicationFinished -> true + else -> false } @@ -159,17 +167,6 @@ enum class IDPErrorCodes(val code: String) { class AuthenticationUseCase( private val idpUseCase: IdpUseCase ) { - - @Requirement( - "A_20172#1", - "A_20526-01#1", - "A_20283-01#2", - "A_20167", - "A_19938-01#2", - "GS-A_5322", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Authenticate to the IDP using the health card certificate." - ) fun authenticateWithHealthCard( profileId: ProfileIdentifier, scope: IdpScope = IdpScope.Default, @@ -193,10 +190,6 @@ class AuthenticationUseCase( } @Requirement( - "A_20172#2", - "A_20526-01#2", - "A_20700-07#2", - "A_20700-07#3", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Authenticate to the IDP using the health card certificate." ) @@ -223,7 +216,7 @@ class AuthenticationUseCase( } } - fun authenticateWithSecureElement(profileId: ProfileIdentifier, scope: IdpScope) = + fun authenticateWithSecureElement(profileId: ProfileIdentifier, scope: IdpScope): Flow = alternateAuthenticationFlowWithSecureElement(profileId, scope) .onEach { Napier.d("AuthenticationState: $it") } .catch { cause -> @@ -246,7 +239,11 @@ class AuthenticationUseCase( val healthCardCertificateChannel = Channel() val signChannel = Channel() val responseChannel = Channel() - + @Requirement( + "O.Auth_3#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Description of how the card communication is established." + ) // // + - IDP communication --------- + ------------- + -- + -------- + // / ^ | ^ \ @@ -381,15 +378,18 @@ class AuthenticationUseCase( is CancellationException, is AuthenticationException, is ResponseException -> throw e + else -> { when (e) { is ApiCallException -> handleApiCallException(e, kind) + is KVNRAlreadyAssignedException -> throw AuthenticationException( kind = AuthenticationExceptionKind.InsuranceIdentifierAlreadyAssigned, cause = e ) + else -> throw AuthenticationException(kind) } @@ -397,6 +397,11 @@ class AuthenticationUseCase( } } + @Requirement( + "A_19937#4", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Handling of errors from the IDP Api Call." + ) private fun handleApiCallException(e: ApiCallException, kind: AuthenticationExceptionKind) { val code = e.response.errorBody() ?.let { @@ -411,12 +416,15 @@ class AuthenticationUseCase( when (code) { IDPErrorCodes.AltAuthNotSuccessful -> throw AuthenticationException(AuthenticationExceptionKind.IDPCommunicationAltAuthNotSuccessful) + IDPErrorCodes.InvalidHealthCardCertificate -> throw AuthenticationException(AuthenticationExceptionKind.IDPCommunicationInvalidCertificate) + IDPErrorCodes.InvalidOCSPResponseOfHealthCardCertificate -> throw AuthenticationException( AuthenticationExceptionKind.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate ) + else -> throw AuthenticationException(kind) } @@ -435,10 +443,13 @@ class AuthenticationUseCase( is KeyPermanentlyInvalidatedException, is UserNotAuthenticatedException -> throw AuthenticationException(AuthenticationExceptionKind.UserNotAuthenticated) + is AltAuthenticationCryptoException -> throw AuthenticationException(AuthenticationExceptionKind.SecureElementFailure) + is ApiCallException -> handleApiCallException(e, AuthenticationExceptionKind.IDPCommunicationFailed) + else -> throw AuthenticationException(AuthenticationExceptionKind.IDPCommunicationFailed) } @@ -475,10 +486,13 @@ class AuthenticationUseCase( ) } } + ResponseStatus.WRONG_SECRET_WARNING_COUNT_02 -> throw AuthenticationException(AuthenticationExceptionKind.HealthCardPin2RetriesLeft) + ResponseStatus.WRONG_SECRET_WARNING_COUNT_01 -> throw AuthenticationException(AuthenticationExceptionKind.HealthCardPin1RetryLeft) + else -> { throw AuthenticationException(AuthenticationExceptionKind.HealthCardBlocked) } @@ -487,31 +501,24 @@ class AuthenticationUseCase( send(AuthenticationState.HealthCardCommunicationFinished) } + @Suppress("CyclomaticComplexMethod") private fun handleException(e: Throwable): AuthenticationState = when (e) { is CancellationException -> throw e is AuthenticationException -> { when (e.kind) { - AuthenticationExceptionKind.IDPCommunicationFailed -> - AuthenticationState.IDPCommunicationFailed - AuthenticationExceptionKind.IDPCommunicationAltAuthNotSuccessful -> - AuthenticationState.IDPCommunicationAltAuthNotSuccessful - AuthenticationExceptionKind.IDPCommunicationInvalidCertificate -> - AuthenticationState.IDPCommunicationInvalidCertificate - - AuthenticationExceptionKind.HealthCardBlocked -> - AuthenticationState.HealthCardBlocked - AuthenticationExceptionKind.HealthCardPin1RetryLeft -> - AuthenticationState.HealthCardPin1RetryLeft - AuthenticationExceptionKind.HealthCardPin2RetriesLeft -> - AuthenticationState.HealthCardPin2RetriesLeft - AuthenticationExceptionKind.HealthCardCommunicationFailed -> - AuthenticationState.HealthCardCommunicationInterrupted - AuthenticationExceptionKind.SecureElementFailure -> - AuthenticationState.SecureElementCryptographyFailed + AuthenticationExceptionKind.IDPCommunicationFailed -> AuthenticationState.IDPCommunicationFailed + AuthenticationExceptionKind.IDPCommunicationAltAuthNotSuccessful -> AuthenticationState.IDPCommunicationAltAuthNotSuccessful + AuthenticationExceptionKind.IDPCommunicationInvalidCertificate -> AuthenticationState.IDPCommunicationInvalidCertificate + AuthenticationExceptionKind.HealthCardBlocked -> AuthenticationState.HealthCardBlocked + AuthenticationExceptionKind.HealthCardPin1RetryLeft -> AuthenticationState.HealthCardPin1RetryLeft + AuthenticationExceptionKind.HealthCardPin2RetriesLeft -> AuthenticationState.HealthCardPin2RetriesLeft + AuthenticationExceptionKind.HealthCardCommunicationFailed -> AuthenticationState.HealthCardCommunicationInterrupted + AuthenticationExceptionKind.SecureElementFailure -> AuthenticationState.SecureElementCryptographyFailed AuthenticationExceptionKind.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate + AuthenticationExceptionKind.UserNotAuthenticated -> AuthenticationState.UserNotAuthenticated AuthenticationExceptionKind.InsuranceIdentifierAlreadyAssigned -> { val alreadyAssignedException = (e.cause!! as KVNRAlreadyAssignedException) AuthenticationState.InsuranceIdentifierAlreadyExists( @@ -520,19 +527,21 @@ class AuthenticationUseCase( insuranceIdentifier = alreadyAssignedException.insuranceIdentifier ) } - AuthenticationExceptionKind.UserNotAuthenticated -> AuthenticationState.UserNotAuthenticated } } + is ResponseException -> { when (e.responseStatus) { ResponseStatus.AUTHENTICATION_FAILURE -> AuthenticationState.HealthCardCardAccessNumberWrong else -> AuthenticationState.HealthCardCommunicationInterrupted } } + is TagLostException, is IOException -> { Napier.e("IO Exception / NFC TAG was lost", e) AuthenticationState.HealthCardCommunicationInterrupted } + else -> { Napier.e("Unknown exception", e) // soft fail diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt index b8fef4e8..1ff738dd 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt index adf12c10..c6a2295a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.usecase @@ -45,7 +45,7 @@ open class CardWallUseCase( // On some devices, the isEnabled() method of the NFCManager throws an exception // https://stackoverflow.com/questions/23564475/check-programmatically-if-device-has-nfc-reader - fun checkNfcEnabled(): Boolean = riskyOperation( + fun isNfcEnabled(): Boolean = riskyOperation( block = applicationModule.androidContext()::isNfcEnabled, defaultValue = false ) ?: false diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt index 880a0836..a131175b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt index 14ad84c9..da527115 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.usecase.model diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/components/ErezeptTopAppBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/components/ErezeptTopAppBar.kt deleted file mode 100644 index 33a8e3a4..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/components/ErezeptTopAppBar.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.NavigationBack -import de.gematik.ti.erp.app.utils.compose.NavigationClose - -object ErezeptTopAppBar { - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Close( - title: String?, - onClickNavIcon: () -> Unit - ) { - TopAppBar( - title = { - title?.let { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart - ) { - Text( - style = AppTheme.typography.h6, - overflow = TextOverflow.Ellipsis, - text = it - ) - } - } - }, - navigationIcon = { - NavigationClose { onClickNavIcon() } - } - ) - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Back( - title: String, - onClickNavIcon: () -> Unit - ) { - TopAppBar( - title = { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart - ) { - Text( - style = AppTheme.typography.h6, - overflow = TextOverflow.Ellipsis, - text = title - ) - } - }, - navigationIcon = { - NavigationBack { onClickNavIcon() } - } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt index 7d9ee0dc..21883288 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MagicNumber") @@ -24,6 +24,7 @@ import androidx.activity.ComponentActivity import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.calculateCentroidSize import androidx.compose.foundation.gestures.calculatePan @@ -31,10 +32,13 @@ import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -42,6 +46,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.blur import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color @@ -54,51 +59,94 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.toSize +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.google.accompanist.navigation.material.BottomSheetNavigator +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.google.accompanist.systemuicontroller.rememberSystemUiController import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.base.falseStateFlow import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator import de.gematik.ti.erp.app.demomode.DemoModeIntent import de.gematik.ti.erp.app.demomode.startAppWithNormalMode import de.gematik.ti.erp.app.demomode.ui.DemoModeStatusBar import de.gematik.ti.erp.app.demomode.ui.checkForDemoMode import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.presentation.rememberAccessibilitySettingsController +import de.gematik.ti.erp.app.settings.presentation.rememberSettingsController import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone import org.kodein.di.DI import kotlin.math.max import kotlin.math.min +val LocalBiometricAuthenticator = + staticCompositionLocalOf { error("No BiometricAuthenticator provided!") } + val LocalAuthenticator = staticCompositionLocalOf { error("No authenticator provided!") } val LocalActivity = - staticCompositionLocalOf { error("No activity provided!") } + staticCompositionLocalOf { error("No ComponentActivity provided!") } val LocalAnalytics = - staticCompositionLocalOf { error("No analytics provided!") } + staticCompositionLocalOf { error("No Analytics provided!") } val LocalDi = staticCompositionLocalOf { error("No DI provided!") } +val LocalTimeZone = staticCompositionLocalOf { error("No Timezone provided!") } + +@OptIn(ExperimentalMaterialNavigationApi::class) +val LocalBottomSheetNavigator = + staticCompositionLocalOf { error("No BottomSheetNavigator provided!") } + +@OptIn(ExperimentalMaterialApi::class) +val LocalBottomSheetNavigatorSheetState = + staticCompositionLocalOf { error("No BottomSheetNavigator provided!") } + +val LocalNavController = + staticCompositionLocalOf { error("No NavHostController provided!") } @Composable fun AppContent( content: @Composable () -> Unit ) { - val settingsController = rememberAccessibilitySettingsController() - val zoomState by settingsController.zoomState + var isAppInBackground by remember { mutableStateOf(false) } + val settingsController = rememberSettingsController() + val zoomState by settingsController.zoomState.collectAsStateWithLifecycle() AppTheme { val systemUiController = rememberSystemUiController() val useDarkIcons = MaterialTheme.colors.isLight - val activity = LocalActivity.current + val activity = LocalActivity.current as? BaseActivity + val isZoomDisabledTemporarily by activity?.disableZoomTemporarily?.collectAsState() ?: falseStateFlow + SideEffect { systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = useDarkIcons) } + LifecycleEventObserver { event -> + when (event) { + Lifecycle.Event.ON_RESUME, Lifecycle.Event.ON_START -> { + isAppInBackground = false + } + + Lifecycle.Event.ON_PAUSE, Lifecycle.Event.ON_STOP -> { + isAppInBackground = true + } + + else -> { + // do nothing + } + } + } checkForDemoMode( demoModeStatusBarColor = AppTheme.colors.yellow500, demoModeContent = { @@ -108,11 +156,15 @@ fun AppContent( textColor = AppTheme.colors.neutral900, demoModeActiveText = stringResource(R.string.demo_mode_text), demoModeEndText = stringResource(R.string.demo_mode_cancel_button_text), - onClickDemoModeEnd = { DemoModeIntent.startAppWithNormalMode(activity) } + onClickDemoModeEnd = { activity?.let { DemoModeIntent.startAppWithNormalMode(it) } } ) }, appContent = { - Box(modifier = Modifier.zoomable(enabled = zoomState.zoomEnabled)) { + Box( + modifier = Modifier + .switchBlur(isAppInBackground) + .zoomable(enabled = zoomState.zoomEnabled && !isZoomDisabledTemporarily) + ) { content() } } @@ -120,7 +172,15 @@ fun AppContent( } } -fun Modifier.zoomable( +@Suppress("MagicNumber", "UnusedPrivateMember") +private fun Modifier.switchBlur(isAppInBackground: Boolean) = this.then( + Modifier + .blur(if (isAppInBackground) SizeDefaults.sixfoldAndQuarter else SizeDefaults.zero) + .graphicsLayer { alpha = if (isAppInBackground) 0.5f else 1f } + .background(if (isAppInBackground) Color.Gray.copy(alpha = 0.5f) else Color.Transparent) +) + +private fun Modifier.zoomable( minZoom: Float = 1f, maxZoom: Float = 3.5f, delayInMillis: Long = 1500, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt index 31f2656b..cb50255c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.core diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt index e4866f8f..487aad43 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.core @@ -25,6 +25,12 @@ import android.net.Uri import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.demomode.DemoModeIntentAction +import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeEnded +import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeStarted +import de.gematik.ti.erp.app.idp.api.models.UniversalLinkToken.Companion.toUniversalLinkToken +import de.gematik.ti.erp.app.medicationplan.worker.REMINDER_NOTIFICATION_INTENT_ACTION import io.github.aakira.napier.Napier import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow @@ -34,36 +40,76 @@ const val ExternalAppAuthenticationBaseUri = "https://das-e-rezept-fuer-deutschl const val WwwExternalAppAuthenticationBaseUri = "https://www.das-e-rezept-fuer-deutschland.de/extauth" const val ShareBaseUri = "https://das-e-rezept-fuer-deutschland.de/prescription" +data class GidResultIntent( + val uriData: String, + val resultChannel: Channel +) + @Stable class IntentHandler(private val context: Context) { - private val extAuthChannel = Channel(Channel.CONFLATED) + private val extAuthChannel = Channel(Channel.CONFLATED) private val shareChannel = Channel(Channel.CONFLATED) + private val gidSuccessfulChannel = Channel(Channel.CONFLATED) val extAuthIntent = extAuthChannel.receiveAsFlow() - val shareIntent = shareChannel.receiveAsFlow() + val gidSuccessfulIntent = gidSuccessfulChannel.receiveAsFlow() @Requirement( - "O.Source_1#5", + "O.Source_1#7", sourceSpecification = "BSI-eRp-ePA", - rationale = "External application calls via Universal Linking" + rationale = "Processing of universal link", + codeLines = 7 ) + @Suppress("NestedBlockDepth") suspend fun propagateIntent(intent: Intent) { + if (intent.validateForDemoMode()) { + if (intent.action == DemoModeStarted.name) { + (context as BaseActivity).setAsDemoMode() + (context).analytics.init(context) + } else if (intent.action == DemoModeEnded.name) { + (context as BaseActivity).cancelDemoMode() + } + } else { + (context as BaseActivity).cancelDemoMode() + (context).analytics.init(context) + if (intent.action == REMINDER_NOTIFICATION_INTENT_ACTION) { + (context).shouldShowMedicationSuccess() + } + } + intent.data?.let { - val value = it.toString() - Napier.d("Received new intent: $value") + val data = it.toString() + Napier.d("Received new intent: $data") - when { - value.startsWith(ExternalAppAuthenticationBaseUri) || - value.startsWith(WwwExternalAppAuthenticationBaseUri) -> - extAuthChannel.send(value) + if (data.isValidUri()) { + when { + data.startsWith(ExternalAppAuthenticationBaseUri) || data.startsWith( + WwwExternalAppAuthenticationBaseUri + ) -> { + if (URI(data).validateForUniversalLink()) { + extAuthChannel.send( + GidResultIntent( + uriData = data, + resultChannel = gidSuccessfulChannel + ) + ) + } + } - value.startsWith(ShareBaseUri) -> - shareChannel.send(value) + data.startsWith(ShareBaseUri) -> { + shareChannel.send(data) + } + } } } } + @Requirement( + "O.Auth_4#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Start the external app." + ) fun tryStartingExternalHealthInsuranceAuthenticationApp( redirect: URI, onSuccess: () -> Unit, @@ -82,8 +128,39 @@ class IntentHandler(private val context: Context) { private fun clear() { extAuthChannel.tryReceive() shareChannel.tryReceive() + gidSuccessfulChannel.tryReceive() } } +@Requirement( + "O.Source_1#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "All parameters are mandatory for the universal link token are checked before it is sent for processing", + codeLines = 2 +) +fun URI.validateForUniversalLink(): Boolean = this.toUniversalLinkToken() != null + +@Requirement( + "O.Source_1#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "External uri is validated to be a valid uri", + codeLines = 7 +) +private fun String.isValidUri(): Boolean = try { + URI(this) + true +} catch (e: Exception) { + false +} + +@Requirement( + "O.Source_1#8", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Demo mode is started or ended with an intent and it is also checked that no other data is present in the intent." +) +fun Intent.validateForDemoMode(): Boolean = + scheme == null && extras == null && + (action == DemoModeIntentAction.DemoModeStarted.name || action == DemoModeIntentAction.DemoModeEnded.name) + val LocalIntentHandler = staticCompositionLocalOf { error("No intent handler provided!") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/LifecycleEventObserver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/LifecycleEventObserver.kt new file mode 100644 index 00000000..0d15b64d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/LifecycleEventObserver.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.core + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver + +@Composable +fun LifecycleEventObserver(onEvent: (Lifecycle.Event) -> Unit) { + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + onEvent(event) + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/DebugSettingsData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/DebugSettingsData.kt index 81e1b563..5a3a5fc0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/DebugSettingsData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/DebugSettingsData.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.data +package de.gematik.ti.erp.app.debugsettings.data import android.os.Parcelable import androidx.compose.runtime.Immutable diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/Environment.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/Environment.kt index ef860c09..e4bb86c2 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/Environment.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/data/Environment.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.debugsettings.data diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/di/DebugSettingsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/di/DebugSettingsModule.kt new file mode 100644 index 00000000..1f9bf78f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/di/DebugSettingsModule.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.di + +import de.gematik.ti.erp.app.debugsettings.usecase.BreakSsoTokenUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val debugSettingsModule = DI.Module("debugSettingsModule") { + bindProvider { BreakSsoTokenUseCase(instance(), instance()) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/presentation/LoggerScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/presentation/LoggerScreenController.kt new file mode 100644 index 00000000..0f854351 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/presentation/LoggerScreenController.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.logger.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.logger.SessionLogHolder +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import org.kodein.di.compose.rememberInstance +import java.io.File + +class LoggerScreenController( + private val logHolder: SessionLogHolder +) : Controller() { + val httpLogs by lazy { + logHolder.http + .stateIn( + scope = controllerScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + } + + fun saveHarToFile(harContent: String, filePath: String) { + try { + File(filePath).bufferedWriter().use { it.write(harContent) } + } catch (e: Throwable) { + Napier.e { "error savings logs ${e.stackTraceToString()}" } + throw FileSavingException("Error saving logs: ${e.message}") + } + } +} + +@Composable +fun rememberLoggerScreenController(): LoggerScreenController { + val logHolder by rememberInstance() + return remember { + LoggerScreenController(logHolder) + } +} + +data class FileSavingException(override val message: String) : Exception(message) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/ui/screens/LoggerScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/ui/screens/LoggerScreen.kt new file mode 100644 index 00000000..90adbe51 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/logger/ui/screens/LoggerScreen.kt @@ -0,0 +1,388 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.logger.ui.screens + +import android.os.Build +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Divider +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.base.ClipBoardCopy +import de.gematik.ti.erp.app.debugsettings.logger.presentation.rememberLoggerScreenController +import de.gematik.ti.erp.app.logger.mapper.toHar +import de.gematik.ti.erp.app.logger.mapper.toJson +import de.gematik.ti.erp.app.logger.model.ContentLog +import de.gematik.ti.erp.app.logger.model.HeaderLog +import de.gematik.ti.erp.app.logger.model.LogEntry +import de.gematik.ti.erp.app.logger.model.RequestLog +import de.gematik.ti.erp.app.logger.model.ResponseLog +import de.gematik.ti.erp.app.logger.model.TimingsLog +import de.gematik.ti.erp.app.permissions.getWriteExternalStoragePermissionLauncher +import de.gematik.ti.erp.app.permissions.isWritePermissionGranted +import de.gematik.ti.erp.app.permissions.writeExternalStorage +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import de.gematik.ti.erp.app.utils.extensions.show +import de.gematik.ti.erp.app.utils.extensions.showWithDismissButton +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty +import io.github.aakira.napier.Napier +import kotlinx.coroutines.launch + +object LoggerScreen { + + @Composable + fun Content( + onBack: () -> Unit + ) { + val context = LocalContext.current + val snackbar = LocalSnackbarScaffold.current + val scope = rememberCoroutineScope() + + val listState = rememberLazyListState() + val controller = rememberLoggerScreenController() + + val onWriteStoragePermissionGrantedEvent = ComposableEvent() + + val httpLogs by controller.httpLogs.collectAsStateWithLifecycle() + + val permissionLauncher: ManagedActivityResultLauncher = getWriteExternalStoragePermissionLauncher { isGranted -> + when { + isGranted -> onWriteStoragePermissionGrantedEvent.trigger() + else -> snackbar.showWithDismissButton( + message = "Write permission denied", + actionLabel = "Close", + scope = scope + ) + } + } + + onWriteStoragePermissionGrantedEvent.listen { + val path = "${ContextCompat.getExternalFilesDirs(context, null).first()}" + + "/${BuildKonfig.VERSION_NAME}_logs.har" + try { + val logs = httpLogs.toHar().toJson() + controller.saveHarToFile( + harContent = logs, + filePath = path + ) + } catch (e: Throwable) { + Napier.e { "$e" } + snackbar.show( + message = "Error saving logs", + scope = scope + ) + } finally { + Napier.i { "file path $path" } + snackbar.showWithDismissButton( + message = "Logs saved to $path", + actionLabel = "Close", + duration = SnackbarDuration.Indefinite, + scope = scope + ) + } + } + + AnimatedElevationScaffold( + topBarTitle = "Logger", + navigationMode = NavigationBarMode.Back, + listState = listState, + actions = { + Row { + PrimaryButton( + modifier = Modifier.padding(end = PaddingDefaults.Medium), + enabled = httpLogs.isNotEmpty(), + onClick = { + val logs = httpLogs.toHar().toJson() + ClipBoardCopy.copyToClipboard( + context = context, + text = logs + ) + } + ) { + Text("Copy") + } + PrimaryButton( + modifier = Modifier.padding(end = PaddingDefaults.Medium), + enabled = httpLogs.isNotEmpty(), + onClick = { + if (context.isWritePermissionGranted()) { + onWriteStoragePermissionGrantedEvent.trigger() + } else { + scope.launch { + permissionLauncher.launchBasedOnSdkVersion { + onWriteStoragePermissionGrantedEvent.trigger() + } + } + } + } + ) { + Text("Save") + } + } + }, + onBack = onBack, + content = { + LoggerContent( + listState = listState, + httpLogs = httpLogs + ) + } + ) + } + + @Composable + private fun LoggerContent( + listState: LazyListState, + httpLogs: List + ) { + if (httpLogs.isEmpty()) { + ErrorScreenComponent() + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState + ) { + itemsIndexed(httpLogs) { _, log -> + Log(log) + Divider() + } + item { + SpacerLarge() + } + } + } + } +} + +// After Android 11 scoped storage is forced, so we don't need to request permission to write app based external storage +private fun ManagedActivityResultLauncher.launchBasedOnSdkVersion( + onNoTrigger: () -> Unit +) { + when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> launch(writeExternalStorage) + else -> onNoTrigger() + } +} + +@Composable +private fun TimeStampEntry(entry: LogEntry) { + Text( + text = "Timestamp", + style = AppTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Text( + text = entry.timestamp, + style = AppTheme.typography.caption1 + ) +} + +@Composable +private fun Header( + entry: List +) { + SpacerTiny() + Text( + text = "Headers", + style = AppTheme.typography.subtitle2 + ) + entry.forEach { + Text( + text = "${it.name} ${it.value}", + style = AppTheme.typography.caption1 + ) + } +} + +@Composable +private fun RequestEntry( + entry: LogEntry +) { + Text( + text = "Request", + style = AppTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Text( + text = "method = ${entry.request.method}", + style = AppTheme.typography.caption1 + ) + Text( + text = "url = ${entry.request.url}", + style = AppTheme.typography.caption1 + ) + if (entry.request.headers.isNotEmpty()) { + Header(entry = entry.request.headers) + } +} + +@Composable +private fun ResponseEntry(entry: LogEntry) { + Text( + text = "Response", + style = AppTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + Text( + text = "statuscode = ${entry.response.status}", + style = AppTheme.typography.caption1 + ) + if (entry.response.statusText.isNotNullOrEmpty()) { + Text( + text = "text = ${entry.response.statusText}", + style = AppTheme.typography.caption1 + ) + } + if (entry.response.headers.isNotEmpty()) { + Header(entry = entry.response.headers) + } + SpacerTiny() + Text( + text = "Content", + style = AppTheme.typography.caption2 + ) + Text( + text = "mimeType = ${entry.response.content.mimeType}", + style = AppTheme.typography.caption1 + ) + Text( + text = entry.response.content.text, + style = AppTheme.typography.caption1 + ) +} + +@Composable +private fun TimingsEntry( + entry: LogEntry +) { + Text( + text = "Timings", + style = AppTheme.typography.subtitle1, + fontWeight = FontWeight.Bold + ) + SpacerTiny() + Text( + text = "sent at ${entry.timings.send}", + style = AppTheme.typography.caption1 + ) + Text( + text = "wait time ${entry.timings.wait}", + style = AppTheme.typography.caption1 + ) + Text( + text = "received at ${entry.timings.receive}", + style = AppTheme.typography.caption1 + ) +} + +@Composable +private fun Log( + entry: LogEntry +) { + Column( + modifier = Modifier.padding(PaddingDefaults.Medium) + ) { + TimeStampEntry(entry = entry) + SpacerMedium() + RequestEntry(entry = entry) + SpacerMedium() + ResponseEntry(entry = entry) + SpacerMedium() + TimingsEntry(entry = entry) + SpacerMedium() + } +} + +@LightDarkPreview +@Composable +fun LoggerScreenPreview() { + PreviewAppTheme { + Log( + entry = LogEntry( + timestamp = "2021-09-01T12:00:00", + request = RequestLog( + method = "GET", + url = "https://example.com", + headers = listOf( + HeaderLog( + name = "req header1", + value = "value1" + ), + HeaderLog( + name = "req header2", + value = "value2" + ) + ) + ), + response = ResponseLog( + status = 200, + statusText = "OK", + headers = listOf( + HeaderLog( + name = "header1", + value = "value1" + ), + HeaderLog( + name = "header2", + value = "value2" + ) + ), + content = ContentLog( + mimeType = "text/plain", + text = "response text" + ) + ), + timings = TimingsLog( + send = 1000, + wait = 2000, + receive = 3000 + ) + ) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/model/SsoTokenHeader.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/model/SsoTokenHeader.kt new file mode 100644 index 00000000..da75afcc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/model/SsoTokenHeader.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.model + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class SsoTokenHeader( + val enc: String, + val cty: String, + val exp: Long, + val alg: String, + val kid: String +) { + companion object { + fun toSsoTokenHeader(jsonString: String) = Json.decodeFromString(jsonString) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/DebugScreenNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/DebugScreenNavigation.kt index f4760ec8..a1e0d232 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/DebugScreenNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/DebugScreenNavigation.kt @@ -1,29 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.debugsettings.navigation -import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen import de.gematik.ti.erp.app.navigation.Routes object DebugScreenNavigation { object DebugMain : Routes("DebugMain") object DebugRedeemWithoutFD : Routes("DebugRedeemWithoutFD") object DebugPKV : Routes("DebugPKV") - object DebugTimeout : Routes(DebugTimeoutScreen::class::java.name) + object DebugTimeout : Routes("TimeoutScreen") + object QrCodeScannerScreen : Routes("QrCodeScannerScreen") + object LoggerScreen : Routes("LoggerScreen") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreenRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreenRoutes.kt deleted file mode 100644 index 5a37b13a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreenRoutes.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.navigation - -import de.gematik.ti.erp.app.navigation.NavigationRouteNames -import de.gematik.ti.erp.app.navigation.NavigationRoutes -import de.gematik.ti.erp.app.navigation.Routes - -object SampleScreenRoutes : NavigationRoutes { - override fun subGraphName(): String { - return "sample-screens" - } - - object SampleOverviewsScreen : Routes(NavigationRouteNames.SampleOverviewScreen.name) - object BottomSheetSampleScreen : Routes( - NavigationRouteNames.BottomSheetSampleScreen.name - ) - - object BottomSheetSampleSmallScreen : Routes( - NavigationRouteNames.BottomSheetSampleSmallScreen.name - ) - - object BottomSheetSampleLargeScreen : Routes( - NavigationRouteNames.BottomSheetSampleLargeScreen.name - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreensGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreensGraph.kt deleted file mode 100644 index 01e4fa6f..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/SampleScreensGraph.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.navigation -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.AdaptableHeight -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.FullScreenHeight -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.SmallHeight -import de.gematik.ti.erp.app.debugsettings.ui.SampleOverviewScreen -import de.gematik.ti.erp.app.navigation.renderBottomSheet -import de.gematik.ti.erp.app.navigation.renderComposable -import de.gematik.ti.erp.app.navigation.slideInDown -import de.gematik.ti.erp.app.navigation.slideOutUp - -/** - * This graph shows the different UI components that are possible in the app. - * The different dimensions and states of them. - * Every new screen added here is an example of a new component and some information about it - */ -fun NavGraphBuilder.exampleScreensGraph( - navController: NavController -) { - navigation( - startDestination = SampleScreenRoutes.SampleOverviewsScreen.route, - route = SampleScreenRoutes.subGraphName() - ) { - renderComposable( - route = SampleScreenRoutes.SampleOverviewsScreen.route, - stackEnterAnimation = { slideInDown() }, - stackExitAnimation = { slideOutUp() } - ) { - SampleOverviewScreen( - navController, - it - ) - } - renderBottomSheet( - route = SampleScreenRoutes.BottomSheetSampleScreen.route - ) { - BottomSheetSampleScreen( - navController = navController, - navBackStackEntry = it, - height = AdaptableHeight - ) - } - renderBottomSheet( - route = SampleScreenRoutes.BottomSheetSampleSmallScreen.route - ) { - BottomSheetSampleScreen( - navController = navController, - navBackStackEntry = it, - height = SmallHeight - ) - } - renderBottomSheet( - route = SampleScreenRoutes.BottomSheetSampleLargeScreen.route - ) { - BottomSheetSampleScreen( - navController = navController, - navBackStackEntry = it, - height = FullScreenHeight - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensGraph.kt new file mode 100644 index 00000000..d8c409cd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensGraph.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.debugsettings.showcase.ui.screens.BottomSheetShowcaseScreen +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideOutUp + +/** + * This graph shows the different UI components that are possible in the app. + * The different dimensions and states of them. + * Every new screen added here is an example of a new component and some information about it + */ +fun NavGraphBuilder.showcaseScreensGraph( + navController: NavController +) { + navigation( + startDestination = ShowcaseScreensRoutes.BottomSheetShowcaseScreen.route, + route = ShowcaseScreensRoutes.subGraphName() + ) { + renderComposable( + route = ShowcaseScreensRoutes.BottomSheetShowcaseScreen.route, + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() } + ) { + BottomSheetShowcaseScreen( + navController, + it + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensRoutes.kt new file mode 100644 index 00000000..6fc2bf92 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/navigation/ShowcaseScreensRoutes.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.navigation + +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes + +object ShowcaseScreensRoutes : NavigationRoutes { + override fun subGraphName(): String { + return "showcase-screens" + } + + object BottomSheetShowcaseScreen : Routes(NavigationRouteNames.BottomSheetShowcaseScreen.name) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/presentation/DebugPkvController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/presentation/DebugPkvController.kt new file mode 100644 index 00000000..60465f06 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/presentation/DebugPkvController.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.pkv.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.IsProfilePKVUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToGKVUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.Data +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.Loading +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Suppress("ConstructorParameterNaming") +class DebugPkvController( + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val switchProfileToPKVUseCase: SwitchProfileToPKVUseCase, + private val switchProfileToGKVUseCase: SwitchProfileToGKVUseCase, + private val isProfilePKVUseCase: IsProfilePKVUseCase, + private val _activeProfile: MutableStateFlow> = MutableStateFlow(Loading()) +) : GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = { activeProfile, scope -> + scope.launch { + _activeProfile.value = Data(activeProfile) + } + }, + onFailure = { error, scope -> + scope.launch { + Napier.e { "error on dedug pkv, ${error.message}" } + } + } +) { + private val _isProfilePkv: MutableStateFlow> = MutableStateFlow(Loading()) + val isProfilePKV: StateFlow> = _isProfilePkv + + fun checkIsProfilePkv() { + controllerScope.launch { + activeProfile.collectLatest { + it.data?.let { profile -> + runCatching { + isProfilePKVUseCase.invoke(profile.id) + }.fold( + onSuccess = { result -> + _isProfilePkv.value = Data(result) + }, + onFailure = { + Napier.e { "error on checking pkv, ${it.message}" } + _isProfilePkv.value = Data(false) + } + ) + } + } + } + } + + fun switchToPkv() { + controllerScope.launch { + activeProfile.value.data?.let { profile -> + runCatching { + switchProfileToPKVUseCase.invoke(profile.id) + }.onSuccess { result -> + _isProfilePkv.value = Data(result) + }.onFailure { + Napier.e { "error on changing to pkv, ${it.message}" } + _isProfilePkv.value = Data(false) + } + } + } + } + + fun switchToGkv() { + controllerScope.launch { + activeProfile.value.data?.let { profile -> + runCatching { + switchProfileToGKVUseCase.invoke(profile.id) + }.onSuccess { result -> + if (result) { + _isProfilePkv.value = Data(false) + } else { + _isProfilePkv.value = Data(true) + } + }.onFailure { + Napier.e { "error on changing to pkv, ${it.message}" } + _isProfilePkv.value = Data(false) + } + } + } + } +} + +@Composable +fun rememberDebugPkvController(): DebugPkvController { + val getActiveProfileUseCase by rememberInstance() + val switchProfileToPKVUseCase by rememberInstance() + val switchProfileToGKVUseCase by rememberInstance() + val isProfilePKVUseCase by rememberInstance() + + return remember { + DebugPkvController( + getActiveProfileUseCase = getActiveProfileUseCase, + switchProfileToPKVUseCase = switchProfileToPKVUseCase, + switchProfileToGKVUseCase = switchProfileToGKVUseCase, + isProfilePKVUseCase = isProfilePKVUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/ui/DebugPKV.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/ui/DebugPKV.kt new file mode 100644 index 00000000..b456cc3e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/pkv/ui/DebugPKV.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.pkv.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.debugsettings.pkv.presentation.rememberDebugPkvController +import de.gematik.ti.erp.app.debugsettings.ui.components.DebugCard +import de.gematik.ti.erp.app.debugsettings.ui.components.LoadingButton +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Center +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine + +@Suppress("MagicNumber") +@Composable +fun DebugScreenPKV( + onSaveInvoiceBundle: (String) -> Unit, + onBack: () -> Unit +) { + val listState = rememberLazyListState() + val controller = rememberDebugPkvController() + + LaunchedEffect(Unit) { + controller.checkIsProfilePkv() + } + + val activeProfile by controller.activeProfile.collectAsStateWithLifecycle() + val isProfilePKV by controller.isProfilePKV.collectAsStateWithLifecycle() + + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + listState = listState, + topBarTitle = "Private Insurance (PKV)", + onBack = onBack + ) { innerPadding -> + var invoiceBundle by remember { mutableStateOf("") } + + UiStateMachine(state = activeProfile) { activeProfile -> + LazyColumn( + state = listState, + modifier = Modifier + .padding(innerPadding) + .navigationBarsPadding() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + item { + DebugCard(title = "Switch Insurance") { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Current Profile: ${activeProfile.name}", + textAlign = TextAlign.Center + ) + SpacerMedium() + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = controller::switchToPkv, + modifier = Modifier + .height(SizeDefaults.sevenfoldAndHalf) + .weight(0.4f) + ) { + Center { + Text( + text = "As PKV", + textAlign = TextAlign.Center + ) + } + } + + SpacerMedium() + + Button( + onClick = controller::switchToGkv, + modifier = Modifier + .height(SizeDefaults.sevenfoldAndHalf) + .weight(0.4f) + ) { + Center { + Text( + text = "As GKV", + textAlign = TextAlign.Center + ) + } + } + } + SpacerMedium() + UiStateMachine( + state = isProfilePKV, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onError = { + Text( + modifier = Modifier.fillMaxWidth(), + text = "Cannot check insurance type due to error: ${it.message}", + textAlign = TextAlign.Center + ) + }, + onContent = { isPkv -> + Text( + modifier = Modifier.fillMaxWidth(), + text = if (isPkv) "PKV Profile" else "GKV Profile", + textAlign = TextAlign.Center + ) + } + ) + } + } + item { + DebugCard( + title = "Invoice Bundle" + ) { + ErezeptOutlineText( + modifier = Modifier.fillMaxWidth(), + value = invoiceBundle, + label = "Bundle", + onValueChange = { + invoiceBundle = it + }, + maxLines = 1 + ) + LoadingButton( + onClick = { onSaveInvoiceBundle(invoiceBundle) }, + text = "Save Invoice" + ) + } + } + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeAnalyzer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeAnalyzer.kt new file mode 100644 index 00000000..24dc7320 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeAnalyzer.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.qrcode + +import android.graphics.ImageFormat +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.MultiFormatReader +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import io.github.aakira.napier.Napier +import java.nio.ByteBuffer + +/** + * QR / Data-matrix code analyzer which processes the image and detects the code + * @param onQrCodeDetected callback when a QR code is detected + * @param onQrCodeNotDetected callback when no QR code is detected + */ +class QrCodeAnalyzer( + private val onQrCodeDetected: (String) -> Unit, + private val onQrCodeNotDetected: () -> Unit +) : ImageAnalysis.Analyzer { + + private val supportedImageFormats = listOf( + ImageFormat.YUV_420_888, + ImageFormat.YUV_422_888, + ImageFormat.YUV_444_888 + ) + + override fun analyze(image: ImageProxy) { + if (image.format in supportedImageFormats) { + val bytes = image.planes.first().buffer.toByteArray() + val luminanceSource = PlanarYUVLuminanceSource( + bytes, + image.width, + image.height, + 0, + 0, + image.width, + image.height, + false + ) + val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource)) + try { + val result = MultiFormatReader().apply { + setHints( + mapOf( + DecodeHintType.POSSIBLE_FORMATS to arrayListOf( + BarcodeFormat.QR_CODE, + BarcodeFormat.DATA_MATRIX + ) + ) + ) + }.decode(binaryBitmap) + Napier.d { "QrCodeAnalyzer result ${result.text}" } + onQrCodeDetected(result.text.trim()) + } catch (e: Exception) { + onQrCodeNotDetected() + } finally { + image.close() + } + } + } + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { + get(it) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScanner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScanner.kt new file mode 100644 index 00000000..7987e945 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScanner.kt @@ -0,0 +1,398 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.qrcode + +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.core.resolutionselector.ResolutionSelector +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import io.github.aakira.napier.Napier +import kotlinx.coroutines.launch + +private const val MAX_WEIGHT = 0.9f +private const val MIN_WEIGHT = 0.1f +private const val MID_WEIGHT = 0.4f + +@Composable +fun QrCodeScanner( + onSaveCertificate: (String) -> Unit, + onSavePrivateKey: (String) -> Unit, + onBack: () -> Unit +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } + + var result by remember { mutableStateOf("") } + var closeBottomBar by remember { mutableStateOf(false) } + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.CAMERA + ) == android.content.pm.PackageManager.PERMISSION_GRANTED + ) + } + + // very simple permission handling + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + hasCameraPermission = isGranted + } + ) + LaunchedEffect(true) { + launcher.launch(android.Manifest.permission.CAMERA) + } + Scaffold( + snackbarHost = { + SnackbarHost(snackbarHostState) + }, + topBar = { + NavigationTopAppBar( + navigationMode = NavigationBarMode.Close, + title = "QR Code Scanner" + ) { + onBack() + } + } + ) { + Column( + modifier = Modifier + .padding(it) + .fillMaxSize() + ) { + if (hasCameraPermission) { + AndroidView( + factory = { context -> + val previewView = PreviewView(context) + val preview = Preview.Builder().build() + val selector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + preview.setSurfaceProvider(previewView.surfaceProvider) + val imageAnalysis = ImageAnalysis.Builder() + .setResolutionSelector(ResolutionSelector.Builder().build()) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(context), + QrCodeAnalyzer( + onQrCodeDetected = { qrCode -> + Napier.i { "QrCodeAnalyzer onQrCodeDetected $qrCode" } + closeBottomBar = false + result = qrCode + }, + onQrCodeNotDetected = { + // handle no qr code + } + ) + ) + try { + cameraProviderFuture.get().bindToLifecycle( + lifecycleOwner, + selector, + preview, + imageAnalysis + ) + } catch (e: Exception) { + Napier.e { "Use case binding failed ${e.stackTraceToString()}" } + // handle exception + } + previewView + }, + modifier = Modifier.weight(1f) + ) + } else { + NoCameraPermissionSection() + } + AnimatedVisibility(visible = !closeBottomBar) { + QrCodeScannerBottomBar( + result = result, + onClose = { + closeBottomBar = true + }, + onSaveCertificate = { certificate -> + scope.launch { + snackbarHostState.showSnackbar( + message = "Certificate saved", + duration = SnackbarDuration.Short, + withDismissAction = true + ) + } + onSaveCertificate(certificate) + }, + onSavePrivateKey = { privateKey -> + scope.launch { + snackbarHostState.showSnackbar( + message = "Private Key saved", + duration = SnackbarDuration.Short, + withDismissAction = true + ) + } + onSavePrivateKey(privateKey) + } + ) + } + } + } +} + +@Composable +private fun ColumnScope.NoCameraPermissionSection() { + val context = LocalContext.current + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth(), + textAlign = TextAlign.Center, + color = AppTheme.colors.neutral600, + style = AppTheme.typography.body2, + text = "No camera permission" + ) + SpacerMedium() + ElevatedButton( + modifier = Modifier.padding(PaddingDefaults.Medium), + colors = ButtonDefaults.elevatedButtonColors( + containerColor = AppTheme.colors.primary700 + ), + onClick = { + context.openSettingsAsNewActivity( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS + ) + }, + content = { + Text( + text = "Go to Settings", + color = AppTheme.colors.neutral000, + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.body2 + ) + } + ) + } + } +} + +@Composable +private fun QrCodeScannerBottomBar( + result: String, + onClose: () -> Unit, + onSaveCertificate: (String) -> Unit, + onSavePrivateKey: (String) -> Unit +) { + Column( + modifier = Modifier + .background(AppTheme.colors.neutral100) + .padding(PaddingDefaults.Medium) + .fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(MAX_WEIGHT) + .height(SizeDefaults.doubleHalf), + contentAlignment = Alignment.Center + ) { + Divider( + modifier = Modifier + .width(SizeDefaults.fivefold) + .height(SizeDefaults.quarter), + color = AppTheme.colors.neutral999 + ) + } + IconButton( + modifier = Modifier + .weight(MIN_WEIGHT) + .height(SizeDefaults.doubleHalf), + onClick = onClose + ) { + Icon( + tint = AppTheme.colors.neutral600, + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + } + SpacerSmall() + Text( + text = "Scan Result", + color = AppTheme.colors.neutral600, + maxLines = 1, + style = AppTheme.typography.subtitle1 + ) + SpacerTiny() + Text( + text = result, + modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + maxLines = 5, + color = AppTheme.colors.neutral600, + softWrap = true, + style = AppTheme.typography.body2 + ) + SpacerSmall() + Row { + ElevatedButton( + modifier = Modifier.weight(MID_WEIGHT), + colors = ButtonDefaults.elevatedButtonColors( + containerColor = AppTheme.colors.primary700 + ), + onClick = { + onSaveCertificate(result) + } + ) { + Text( + text = "Save Certificate", + color = AppTheme.colors.neutral000, + style = AppTheme.typography.button + ) + } + SpacerMedium() + ElevatedButton( + modifier = Modifier.weight(MID_WEIGHT), + colors = ButtonDefaults.elevatedButtonColors( + containerColor = AppTheme.colors.primary700 + ), + onClick = { + onSavePrivateKey(result) + } + ) { + Text( + text = "Save Private Key", + color = AppTheme.colors.neutral000, + style = AppTheme.typography.button + ) + } + } + } +} + +@LightDarkPreview +@Composable +fun QrCodeScannerBottomBarPreview() { + PreviewAppTheme { + QrCodeScannerBottomBar( + onSaveCertificate = {}, + onSavePrivateKey = {}, + onClose = {}, + result = "This is a QR Code Result" + ) + } +} + +@LightDarkPreview +@Composable +fun QrCodeScannerBottomBarMoreTextPreview() { + PreviewAppTheme { + QrCodeScannerBottomBar( + onSaveCertificate = {}, + onSavePrivateKey = {}, + onClose = {}, + result = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod " + + "tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. " + + "At vero eos et accusam et justo duo dolores et ea rebum. " + + "Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " + + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + + "ut labore et dolore magna aliquyam erat, sed diam voluptua. " + + "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, " + + "no sea takimata sanctus est Lorem ipsum dolor sit amet." + ) + } +} + +@LightDarkPreview +@Composable +fun NoCameraPermissionSectionPreview() { + PreviewAppTheme { + Column { + NoCameraPermissionSection() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScannerScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScannerScreen.kt new file mode 100644 index 00000000..9ba96282 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/qrcode/QrCodeScannerScreen.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.qrcode + +import androidx.compose.runtime.Composable + +object QrCodeScannerScreen { + @Composable + fun Content( + onSaveCertificate: (String) -> Unit, + onSavePrivateKey: (String) -> Unit, + onBack: () -> Unit + ) { + QrCodeScanner( + onSaveCertificate = onSaveCertificate, + onSavePrivateKey = onSavePrivateKey, + onBack = onBack + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/presentation/BottomSheetShowcaseScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/presentation/BottomSheetShowcaseScreenController.kt new file mode 100644 index 00000000..0fb6da73 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/presentation/BottomSheetShowcaseScreenController.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.showcase.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import io.github.aakira.napier.Napier +import org.kodein.di.compose.rememberInstance + +class BottomSheetShowcaseScreenController( + private val getActiveProfileUseCase: GetActiveProfileUseCase +) : GetActiveProfileController(getActiveProfileUseCase) { + fun init() { + Napier.d { "on init" } + } +} + +@Composable +fun rememberBottomSheetShowcaseScreenController(): BottomSheetShowcaseScreenController { + val getActiveProfileUseCase by rememberInstance() + return remember { + BottomSheetShowcaseScreenController(getActiveProfileUseCase) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/ui/screens/BottomSheetShowcaseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/ui/screens/BottomSheetShowcaseScreen.kt new file mode 100644 index 00000000..8082b311 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/showcase/ui/screens/BottomSheetShowcaseScreen.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.showcase.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Announcement +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.ContactSupport +import androidx.compose.material.icons.automirrored.outlined.Input +import androidx.compose.material.icons.automirrored.outlined.Segment +import androidx.compose.material.icons.automirrored.rounded.More +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.debugsettings.showcase.presentation.rememberBottomSheetShowcaseScreenController +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacyPreviewData +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LabelButton +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import kotlinx.coroutines.launch + +class BottomSheetShowcaseScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + val snackbar = LocalSnackbarScaffold.current + + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + val controller = rememberBottomSheetShowcaseScreenController() + val profileData = controller.activeProfile + + BackHandler { + navController.popBackStack() + } + AnimatedElevationScaffold( + topBarTitle = "Bottom sheet screes", + listState = listState, + onBack = { navController.navigateUp() } + ) { + BottomSheetShowcaseScreenContent( + paddingValues = it, + onClickWelcomeDrawer = { + navController.navigate(PrescriptionRoutes.WelcomeDrawerBottomSheetScreen.path()) + }, + onClickGrantConsent = { + navController.navigate(PrescriptionRoutes.GrantConsentBottomSheetScreen.path()) + }, + onClickPharmacyDetailFromMessage = { + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromMessageScreen.path( + pharmacy = PharmacyPreviewData.ALL_PRESENT_DATA, + taskId = "" + ) + ) + }, + onClickPharmacyDetailFromDetail = { + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.path( + pharmacy = PharmacyPreviewData.ALL_PRESENT_DATA, + taskId = "" + ) + ) + }, + onClickPharmacyFilter = { + navController.navigate( + PharmacyRoutes.PharmacyFilterSheetScreen.path( + showNearbyFilter = true, + navigateWithSearchButton = true + ) + ) + }, + onClickChangeProfilePicture = { + profileData.value.data?.let { activeProfile -> + navController.navigate( + ProfileRoutes.ProfileEditPictureBottomSheetScreen.path(profileId = activeProfile.id) + ) + } ?: run { + scope.launch { + snackbar.showSnackbar("No active profile found") + } + } + }, + onClickChangeProfileName = { + profileData.value.data?.let { activeProfile -> + navController.navigate( + ProfileRoutes.ProfileEditNameBottomSheetScreen.path(profileId = activeProfile.id) + ) + } ?: run { + scope.launch { + snackbar.showSnackbar("No active profile found") + } + } + } + ) + } + } +} + +@Composable +private fun BottomSheetShowcaseScreenContent( + paddingValues: PaddingValues, + onClickWelcomeDrawer: () -> Unit, + onClickGrantConsent: () -> Unit, + onClickPharmacyDetailFromMessage: () -> Unit, + onClickPharmacyDetailFromDetail: () -> Unit, + onClickPharmacyFilter: () -> Unit, + onClickChangeProfilePicture: () -> Unit, + onClickChangeProfileName: () -> Unit +) { + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + item { + SpacerLarge() + } + item { + LabelButton( + Icons.AutoMirrored.Outlined.Announcement, + "Welcome drawer" + ) { + onClickWelcomeDrawer() + } + } + item { + LabelButton( + Icons.AutoMirrored.Outlined.ContactSupport, + "Grant consent" + ) { + onClickGrantConsent() + } + } + item { + LabelButton( + Icons.AutoMirrored.Rounded.More, + "Pharmacy Detail from Message " + ) { + onClickPharmacyDetailFromMessage() + } + } + item { + LabelButton( + Icons.AutoMirrored.Rounded.More, + "Pharmacy Detail from Detail" + ) { + onClickPharmacyDetailFromDetail() + } + } + item { + LabelButton( + Icons.AutoMirrored.Outlined.Segment, + "Pharmacy Filter" + ) { + onClickPharmacyFilter() + } + } + item { + LabelButton( + Icons.AutoMirrored.Outlined.Input, + "Profile change picture" + ) { + onClickChangeProfilePicture() + } + } + item { + LabelButton( + Icons.AutoMirrored.Outlined.Assignment, + "Profile edit name" + ) { + onClickChangeProfileName() + } + } + item { + SpacerLarge() + } + } +} + +@LightDarkPreview +@Composable +fun BottomSheetShowcaseScreenContentPreview() { + PreviewAppTheme { + BottomSheetShowcaseScreenContent( + paddingValues = PaddingValues(), + onClickWelcomeDrawer = {}, + onClickGrantConsent = {}, + onClickPharmacyDetailFromMessage = {}, + onClickPharmacyFilter = {}, + onClickChangeProfilePicture = {}, + onClickChangeProfileName = {}, + onClickPharmacyDetailFromDetail = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt index 87f84b03..cd543a42 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MagicNumber", "LongMethod") @@ -23,19 +23,19 @@ package de.gematik.ti.erp.app.debugsettings.timeout import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Card import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -51,14 +51,15 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.NoError import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsScreenViewModel +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.SpacerXXLargeMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.SpacerXXLargeMedium import de.gematik.ti.erp.app.utils.extensions.LocalDialog -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance object DebugTimeoutScreen { @@ -68,11 +69,11 @@ object DebugTimeoutScreen { ) { val viewmodel by rememberInstance() val dialog = LocalDialog.current - val snackbar = LocalSnackbar.current + val snackbar = LocalSnackbarScaffold.current val activity = LocalActivity.current - val scrollState = rememberScrollState() - val elevated by remember { derivedStateOf { scrollState.value > 0 } } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() val inactivityMetric by viewmodel.inactivityMetricDuration.collectAsStateWithLifecycle() val pauseMetric by viewmodel.pauseMetricDuration.collectAsStateWithLifecycle() @@ -81,168 +82,189 @@ object DebugTimeoutScreen { AnimatedElevationScaffold( topBarTitle = "Timeout settings", actions = {}, - elevated = elevated, + listState = listState, onBack = onBack ) { paddingValues -> - Column( + LazyColumn( + state = listState, modifier = Modifier .fillMaxWidth() .padding(paddingValues) .padding(horizontal = PaddingDefaults.Medium) ) { - SpacerMedium() - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text( - modifier = Modifier.weight(0.4f), - text = "Inactivity timer:", - style = MaterialTheme.typography.body1 - ) - Card( - modifier = Modifier.weight(0.6f), - border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + item { + SpacerMedium() + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - TextButton( - onClick = { - dialog.show { dialog -> - MetricChangeDialog( - label = "Inactivity Timer", - currentValue = inactivityMetric, - onDismissRequest = { - dialog.dismiss() - }, - onValueChanged = { value, duration -> - viewmodel.setInactivityMetric(value, duration) - dialog.dismiss() - snackbar.show("Inactivity timer reset. Restart required") - } - ) + Text( + modifier = Modifier.weight(0.4f), + text = "Inactivity timer:", + style = MaterialTheme.typography.body1 + ) + Card( + modifier = Modifier.weight(0.6f), + border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + ) { + TextButton( + onClick = { + dialog.show { dialog -> + MetricChangeDialog( + label = "Inactivity Timer", + currentValue = inactivityMetric, + onDismissRequest = { + dialog.dismiss() + }, + onValueChanged = { value, duration -> + viewmodel.setInactivityMetric(value, duration) + dialog.dismiss() + scope.launch { + snackbar.showSnackbar("Inactivity timer reset. Restart required") + } + } + ) + } } + ) { + Text( + "$inactivityMetric", + style = MaterialTheme.typography.body1 + ) } - ) { - Text( - "$inactivityMetric", - style = MaterialTheme.typography.body1 - ) } } } - SpacerMedium() - Text( - text = "The inactivity timer is called when the app is running but nothing is clicked", - style = MaterialTheme.typography.subtitle2 - ) - SpacerLarge() - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { + item { + SpacerMedium() Text( - modifier = Modifier.weight(0.4f), - text = "Pause Timer:", - style = MaterialTheme.typography.body1 + text = "The inactivity timer is called when the app is running but nothing is clicked", + style = MaterialTheme.typography.subtitle2 ) - Card( - modifier = Modifier.weight(0.6f), - border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + } + item { + SpacerLarge() + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - TextButton( - onClick = { - dialog.show { dialog -> - MetricChangeDialog( - label = "Pause Timer", - currentValue = pauseMetric, - onDismissRequest = { - dialog.dismiss() - }, - onValueChanged = { value, duration -> - viewmodel.setPauseMetric(value, duration) - dialog.dismiss() - snackbar.show("Pause timer reset. Restart required") - } - ) + Text( + modifier = Modifier.weight(0.4f), + text = "Pause Timer:", + style = MaterialTheme.typography.body1 + ) + Card( + modifier = Modifier.weight(0.6f), + border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + ) { + TextButton( + onClick = { + dialog.show { dialog -> + MetricChangeDialog( + label = "Pause Timer", + currentValue = pauseMetric, + onDismissRequest = { + dialog.dismiss() + }, + onValueChanged = { value, duration -> + viewmodel.setPauseMetric(value, duration) + dialog.dismiss() + scope.launch { + snackbar.showSnackbar("Pause timer reset. Restart required") + } + } + ) + } } + ) { + Text( + "$pauseMetric", + style = MaterialTheme.typography.body1 + ) } + } + } + } + item { + SpacerMedium() + Text( + text = "The pause timer is called when the app is minimized", + style = MaterialTheme.typography.subtitle2 + ) + } + item { + SpacerXXLargeMedium() + TextButton( + modifier = Modifier.padding(horizontal = PaddingDefaults.XXLargeMedium), + border = BorderStroke(1.dp, AppTheme.colors.neutral300), + onClick = { + viewmodel.resetToDefaultMetrics() + scope.launch { + snackbar.showSnackbar("All timers reset. Restart required") + } + } + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { + Image( + painter = painterResource(R.drawable.ic_reset), + contentDescription = null + ) Text( - "$pauseMetric", - style = MaterialTheme.typography.body1 + text = "Reset to Default", + style = MaterialTheme.typography.h6 ) } } } - SpacerMedium() - Text( - text = "The pause timer is called when the app is minimized", - style = MaterialTheme.typography.subtitle2 - ) - SpacerXXLargeMedium() - TextButton( - modifier = Modifier.padding(horizontal = PaddingDefaults.XXLargeMedium), - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - onClick = { - viewmodel.resetToDefaultMetrics() - snackbar.show("All timers reset. Restart required") - } - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(R.drawable.ic_reset), - contentDescription = null - ) - Text( - text = "Reset to Default", - style = MaterialTheme.typography.h6 - ) - } + item { + SpacerXXLarge() + PrimaryButton( + modifier = Modifier + .padding(horizontal = PaddingDefaults.XXLargeMedium) + .fillMaxWidth(), + content = { + Text( + modifier = Modifier, + text = "Restart App", + style = MaterialTheme.typography.h6 + ) + }, + onClick = { + restartApp(activity) + } + ) } - SpacerXXLarge() - PrimaryButton( - modifier = Modifier - .padding(horizontal = PaddingDefaults.XXLargeMedium) - .fillMaxWidth(), - content = { - Text( - modifier = Modifier, - text = "Restart App", - style = MaterialTheme.typography.h6 - ) - }, - onClick = { - restartApp(activity) - } - ) } } - // show errors for timeout - when (error) { - TimeoutsError.InactivityError -> snackbar.show( - text = "Error setting inactivity timer", - backgroundTint = R.color.red_500 - ) + LaunchedEffect(error) { + // show errors for timeout + when (error) { + TimeoutsError.InactivityError -> snackbar.showSnackbar( + message = "Error setting inactivity timer" + // backgroundTint = R.color.red_500 + ) - TimeoutsError.PauseError -> snackbar.show( - text = "Error setting pause timer", - backgroundTint = R.color.red_500 - ) + TimeoutsError.PauseError -> snackbar.showSnackbar( + message = "Error setting pause timer" + // backgroundTint = R.color.red_500 + ) - TimeoutsError.Error -> snackbar.show( - text = "Error setting inactivity and pause timers", - backgroundTint = R.color.red_500 + TimeoutsError.Error -> snackbar.showSnackbar( + message = "Error setting inactivity and pause timers" + // backgroundTint = R.color.red_500 - ) + ) - NoError -> { - // show nothing + NoError -> { + // show nothing + } } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt index a09ea33d..4a9e6919 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.debugsettings.timeout.intent diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt index 3d0bcc00..054e75f9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MagicNumber") @@ -27,14 +27,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Divider import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton @@ -47,24 +45,22 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.HOURS import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.MINUTES import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.SECONDS -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.extensions.ErezeptKeyboardOptions import kotlinx.coroutines.android.awaitFrame import kotlin.time.Duration -@OptIn(ExperimentalComposeUiApi::class) @Composable fun MetricChangeDialog( label: String, @@ -97,13 +93,10 @@ fun MetricChangeDialog( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.weight(0.4f) .focusRequester(focusRequester), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number - ), + keyboardOptions = ErezeptKeyboardOptions.number, keyboardActions = KeyboardActions( onDone = { onValueChanged(textFieldValue.text, selectedDurationEnum) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/BottomSheetSampleScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/BottomSheetSampleScreen.kt deleted file mode 100644 index cfff086e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/BottomSheetSampleScreen.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.AdaptableHeight -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.FullScreenHeight -import de.gematik.ti.erp.app.debugsettings.ui.BottomSheetSampleScreen.BottomSheetSampleHeight.SmallHeight -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.BottomSheetScreen -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class BottomSheetSampleScreen( - private val height: BottomSheetSampleHeight, - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : BottomSheetScreen( - skipPartiallyExpanded = true, - allowStateChange = true -) { - - sealed class BottomSheetSampleHeight(val value: Dp?) { - data object AdaptableHeight : BottomSheetSampleHeight(null) - data object SmallHeight : BottomSheetSampleHeight(200.dp) - data object FullScreenHeight : BottomSheetSampleHeight(1800.dp) - } - - @Composable - override fun Content() { - BackHandler { - navController.popBackStack() - } - BottomSheetSample(height = height) - } -} - -@Composable -private fun BottomSheetSample( - height: BottomSheetSampleScreen.BottomSheetSampleHeight -) { - Box( - modifier = Modifier - .fillMaxWidth() - .then( - if (height != AdaptableHeight) { - Modifier - .height(height.value!!) - .padding(horizontal = PaddingDefaults.Medium) - } else { - Modifier - .padding(horizontal = PaddingDefaults.Medium) - } - ) - - ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SpacerMedium() - Icon( - painter = painterResource(R.drawable.ic_onboarding_logo_gematik), - contentDescription = "onboarding-logo-gematik", - tint = AppTheme.colors.primary900 - ) - SpacerMedium() - when (height) { - AdaptableHeight -> Text( - text = "(Height as per the content)", - style = AppTheme.typography.subtitle1 - ) - - FullScreenHeight -> Text( - text = "(Height = ${height.value})", - style = AppTheme.typography.subtitle1 - ) - - SmallHeight -> Text( - text = "(Height = ${height.value})", - style = AppTheme.typography.subtitle1 - ) - } - SpacerMedium() - Text( - text = "Qu'est-ce que le Lorem Ipsum?", - style = AppTheme.typography.h6 - ) - SpacerMedium() - Text( - text = "Le Lorem Ipsum est simplement du faux texte employé dans la composition et la mise en page " + - "avant impression. Le Lorem Ipsum est le faux texte standard de l'imprimerie depuis les " + - "années 1500, quand un imprimeur anonyme assembla ensemble des morceaux de texte pour " + - "réaliser un livre spécimen de polices de texte. Il n'a pas fait que survivre cinq siècles, " + - "mais s'est aussi adapté à la bureautique informatique, sans que son contenu n'en soit " + - "modifié. Il a été popularisé dans les années 1960 grâce à la vente de feuilles Letraset " + - "contenant des passages du Lorem Ipsum, et, plus récemment, par son inclusion dans des " + - "applications de mise en page de texte, comme Aldus PageMaker.", - style = AppTheme.typography.body1 - ) - SpacerMedium() - } - } -} - -@LightDarkPreview -@Composable -fun BottomSheetSampleAdjustableHeightPreview() { - PreviewAppTheme { - BottomSheetSample(AdaptableHeight) - } -} - -@LightDarkPreview -@Composable -fun BottomSheetSampleFixedHeightPreview() { - PreviewAppTheme { - BottomSheetSample(height = SmallHeight) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugPKV.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugPKV.kt deleted file mode 100644 index 061bda8c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugPKV.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Button -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import kotlinx.coroutines.launch - -@Composable -fun DebugScreenPKV( - onSaveInvoiceBundle: (String) -> Unit, - onBack: () -> Unit -) { - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - navigationMode = NavigationBarMode.Back, - listState = listState, - topBarTitle = "Private Krankenversicherung (PKV)", - onBack = onBack - ) { innerPadding -> - var invoiceBundle by remember { mutableStateOf("") } - val scope = rememberCoroutineScope() - - LazyColumn( - state = listState, - modifier = Modifier - .padding(innerPadding) - .navigationBarsPadding() - .imePadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - contentPadding = PaddingValues(PaddingDefaults.Medium) - ) { - item { - DebugCard(title = "Login state") { - val profilesController = rememberProfileController() - val activeProfile by profilesController.getActiveProfileState() - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Button( - onClick = { - scope.launch { - profilesController.switchToPrivateInsurance(activeProfile.id) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Set User with ${activeProfile.name} as PKV", textAlign = TextAlign.Center) - } - } - } - } - item { - DebugCard( - title = "Invoice Bundle" - ) { - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = invoiceBundle, - label = { Text("Bundle") }, - onValueChange = { - invoiceBundle = it - }, - maxLines = 1 - ) - LoadingButton( - onClick = { onSaveInvoiceBundle(invoiceBundle) }, - text = "Save Invoice" - ) - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentSetButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentSetButton.kt deleted file mode 100644 index 07812139..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentSetButton.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color - -@Composable -fun EditablePathComponentSetButton( - modifier: Modifier = Modifier, - label: String, - text: String, - active: Boolean, - onValueChange: (String, Boolean) -> Unit, - onClick: () -> Unit -) { - val color = if (active) Color.Green else Color.Red - val buttonText = if (active) "SAVED" else "SET" - EditablePathComponentWithControl( - modifier = modifier, - label = label, - textFieldValue = text, - onValueChange = onValueChange, - content = { - Button( - onClick = onClick, - colors = ButtonDefaults.buttonColors(backgroundColor = color), - enabled = !active - - ) { - Text(text = buttonText) - } - } - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentWithControl.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentWithControl.kt deleted file mode 100644 index c0f57749..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EditablePathComponentWithControl.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import de.gematik.ti.erp.app.theme.PaddingDefaults - -@Composable -fun EditablePathComponentWithControl( - modifier: Modifier, - label: String, - textFieldValue: String, - onValueChange: (String, Boolean) -> Unit, - content: @Composable ((Boolean) -> Unit) -> Unit -) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) { - TextField( - value = textFieldValue, - onValueChange = { onValueChange(it, false) }, - label = { Text(label) }, - maxLines = 3, - modifier = Modifier - .weight(1f) - .padding(end = PaddingDefaults.Medium) - ) - - content { onValueChange(textFieldValue, it) } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/LoadingButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/LoadingButton.kt deleted file mode 100644 index e6b8d51d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/LoadingButton.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.material.Button -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.AlertDialog -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import kotlinx.coroutines.launch - -@Composable -fun LoadingButton( - modifier: Modifier = Modifier, - enabled: Boolean = true, - text: String, - onClick: suspend () -> Unit -) { - var loading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(null) } - errorMessage?.let { error -> - AlertDialog( - onDismissRequest = { - errorMessage = null - }, - buttons = { - Button(onClick = { errorMessage = null }) { - Text("OK") - } - }, - text = { - Text(error) - } - ) - } - val scope = rememberCoroutineScope() - - @Suppress("TooGenericExceptionCaught") - Button( - modifier = modifier.fillMaxWidth(), - onClick = { - loading = true - scope.launch { - try { - onClick() - } catch (e: Throwable) { - errorMessage = e.message + (e.cause?.message?.let { " - cause: $it" } ?: "") - } finally { - loading = false - } - } - }, - enabled = enabled && !loading - ) { - if (loading) { - CircularProgressIndicator(Modifier.size(24.dp), strokeWidth = 2.dp, color = AppTheme.colors.neutral600) - SpacerSmall() - } - Text(text, textAlign = TextAlign.Center) - } -} - -@LightDarkPreview -@Composable -fun DebugLoadingButtonPreview() { - PreviewAppTheme { - LoadingButton( - text = "DebugLoadingButton" - ) { } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/SampleOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/SampleOverviewScreen.kt deleted file mode 100644 index b79b4fe4..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/SampleOverviewScreen.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.components.ErezeptTopAppBar -import de.gematik.ti.erp.app.debugsettings.navigation.SampleScreenRoutes -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class SampleOverviewScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - override fun Content() { - BackHandler { - navController.popBackStack() - } - Scaffold( - topBar = { - ErezeptTopAppBar - .Close(title = "UI Components") { - navController.popBackStack() - } - } - ) { - SampleOverviewScreenContent( - paddingValues = it, - onAdaptableScreenClick = { - navController.navigate(SampleScreenRoutes.BottomSheetSampleScreen.path()) - }, - onLargeScreenClick = { - navController.navigate(SampleScreenRoutes.BottomSheetSampleLargeScreen.path()) - }, - onSmallScreenClick = { - navController.navigate(SampleScreenRoutes.BottomSheetSampleSmallScreen.path()) - } - ) - } - } -} - -@Composable -private fun SampleOverviewScreenContent( - paddingValues: PaddingValues, - onAdaptableScreenClick: () -> Unit, - onLargeScreenClick: () -> Unit, - onSmallScreenClick: () -> Unit -) { - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - item { - SpacerLarge() - } - item { - TitleInfoCard( - onClick = onAdaptableScreenClick, - title = "Adaptable bottom sheet", - body = "The height of the content in this bottom sheet will adjust itself based on the content" - ) - } - item { - SpacerMedium() - } - item { - TitleInfoCard( - onClick = onSmallScreenClick, - title = "Small bottom sheet", - body = "The height of this bottomsheet will not grow no matter how much content we add" - ) - } - item { - SpacerMedium() - } - item { - TitleInfoCard( - onClick = onLargeScreenClick, - title = "Full screen bottom sheet", - body = "The height of this bottomsheet will take the full screen even if the content is not that much" - ) - } - item { - SpacerMedium() - } - } -} - -@LightDarkPreview -@Composable -fun SampleOverviewScreenPreview() { - PreviewAppTheme { - Scaffold { - SampleOverviewScreenContent( - onAdaptableScreenClick = {}, - onLargeScreenClick = {}, - onSmallScreenClick = {}, - paddingValues = it - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/TitleInfoCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/TitleInfoCard.kt deleted file mode 100644 index f3d73cc1..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/TitleInfoCard.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.debugsettings.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -@Composable -fun TitleInfoCard( - onClick: () -> Unit, - title: String, - body: String -) { - ElevatedCard( - elevation = CardDefaults.cardElevation( - defaultElevation = 6.dp - ), - colors = CardDefaults.elevatedCardColors( - contentColor = AppTheme.colors.neutral999, - containerColor = AppTheme.colors.neutral050 - ), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - .clickable { onClick() } - ) { - Text( - modifier = Modifier.padding(PaddingDefaults.Medium), - text = title, - textAlign = TextAlign.Center - ) - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - text = body, - style = AppTheme.typography.caption1, - textAlign = TextAlign.Start - ) - } - SpacerMedium() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClearTextTrafficSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClearTextTrafficSection.kt new file mode 100644 index 00000000..d8db5723 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClearTextTrafficSection.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import android.security.NetworkSecurityPolicy +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.capitalizeFirstChar + +@Suppress("MagicNumber") +@Composable +fun ClearTextTrafficSection() { + Row { + Text( + modifier = Modifier.weight(0.7f), + style = AppTheme.typography.subtitle2, + text = "Clear text traffic allowed" + ) + Text( + modifier = Modifier.weight(0.3f), + style = AppTheme.typography.subtitle2, + text = NetworkSecurityPolicy + .getInstance() + .isCleartextTrafficPermitted + .toString() + .capitalizeFirstChar() + ) + } +} + +@LightDarkPreview +@Composable +fun ClearTextTrafficSectionPreview() { + PreviewAppTheme { + ClearTextTrafficSection() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClientIdsSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClientIdsSection.kt new file mode 100644 index 00000000..063e853c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/ClientIdsSection.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun ClientIdsSection() { + Column { + ClientIdItem( + title = "Client ID (PU)", + value = BuildKonfig.CLIENT_ID_PU + ) + SpacerSmall() + ClientIdItem( + title = "Client ID (RU)", + value = BuildKonfig.CLIENT_ID_RU + ) + SpacerSmall() + ClientIdItem( + title = "Client ID (TU)", + value = BuildKonfig.CLIENT_ID_TU + ) + SpacerSmall() + } +} + +@Suppress("MagicNumber") +@Composable +private fun ClientIdItem( + title: String, + value: String +) { + Row(horizontalArrangement = Arrangement.spacedBy(SizeDefaults.triple)) { + Text( + modifier = Modifier.weight(0.4f), + style = AppTheme.typography.subtitle2, + text = title + ) + Text( + modifier = Modifier.weight(0.6f), + style = AppTheme.typography.subtitle2, + text = value + ) + } +} + +@LightDarkPreview +@Composable +fun ClientIdsSectionPreview() { + PreviewAppTheme { + ClientIdsSection() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/DebugCard.kt similarity index 75% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugCard.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/DebugCard.kt index b1b4024f..bc970349 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/DebugCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/DebugCard.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.debugsettings.ui +package de.gematik.ti.erp.app.debugsettings.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,7 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerMedium @Composable fun DebugCard( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentSetButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentSetButton.kt new file mode 100644 index 00000000..f1ab1d41 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentSetButton.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun EditablePathComponentSetButton( + modifier: Modifier = Modifier, + label: String, + text: String, + active: Boolean, + onValueChange: (String, Boolean) -> Unit, + onClick: () -> Unit +) { + val color = if (active) Color.Green else Color.Red + val buttonText = if (active) "SAVED" else "SET" + EditablePathComponentWithControl( + modifier = modifier, + label = label, + textFieldValue = text, + onValueChange = onValueChange, + content = { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors(backgroundColor = color), + enabled = !active + + ) { + Text(text = buttonText) + } + } + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentWithControl.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentWithControl.kt new file mode 100644 index 00000000..0f3e02cf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EditablePathComponentWithControl.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun EditablePathComponentWithControl( + modifier: Modifier, + label: String, + textFieldValue: String, + onValueChange: (String, Boolean) -> Unit, + content: @Composable ((Boolean) -> Unit) -> Unit +) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) { + TextField( + value = textFieldValue, + onValueChange = { onValueChange(it, false) }, + label = { Text(label) }, + maxLines = 3, + modifier = Modifier + .weight(1f) + .padding(end = PaddingDefaults.Medium) + ) + + content { onValueChange(textFieldValue, it) } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EnvironmentSelector.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EnvironmentSelector.kt similarity index 81% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EnvironmentSelector.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EnvironmentSelector.kt index de6e4a5a..715a7ab4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/EnvironmentSelector.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/EnvironmentSelector.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.debugsettings.ui +package de.gematik.ti.erp.app.debugsettings.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -44,6 +44,7 @@ import de.gematik.ti.erp.app.debugsettings.data.Environment import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerXXLarge @Composable fun EnvironmentSelector( @@ -59,6 +60,7 @@ fun EnvironmentSelector( .fillMaxWidth() .selectableGroup() ) { + SpacerXXLarge() Text( text = stringResource(R.string.debug_select_environment), style = AppTheme.typography.h6, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/LoadingButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/LoadingButton.kt new file mode 100644 index 00000000..8240f828 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/LoadingButton.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import kotlinx.coroutines.launch + +@Composable +fun LoadingButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: String, + onClick: suspend () -> Unit +) { + var loading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + + errorMessage?.let { error -> + ErezeptAlertDialog( + title = error, + onDismissRequest = { + errorMessage = null + } + ) + } + val scope = rememberCoroutineScope() + + @Suppress("TooGenericExceptionCaught") + Button( + modifier = modifier.fillMaxWidth(), + onClick = { + loading = true + scope.launch { + try { + onClick() + } catch (e: Throwable) { + errorMessage = e.message + (e.cause?.message?.let { " - cause: $it" } ?: "") + } finally { + loading = false + } + } + }, + enabled = enabled && !loading + ) { + if (loading) { + CircularProgressIndicator(Modifier.size(24.dp), strokeWidth = 2.dp, color = AppTheme.colors.neutral600) + SpacerSmall() + } + Text(text, textAlign = TextAlign.Center) + } +} + +@LightDarkPreview +@Composable +fun DebugLoadingButtonPreview() { + PreviewAppTheme { + LoadingButton( + text = "DebugLoadingButton" + ) { } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/TitleInfoCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/TitleInfoCard.kt new file mode 100644 index 00000000..6f8b53d7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/ui/components/TitleInfoCard.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium + +@Composable +fun TitleInfoCard( + onClick: () -> Unit, + title: String, + body: String +) { + ElevatedCard( + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ), + colors = CardDefaults.elevatedCardColors( + contentColor = AppTheme.colors.neutral999, + containerColor = AppTheme.colors.neutral050 + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + .clickable { onClick() } + ) { + Text( + modifier = Modifier.padding(PaddingDefaults.Medium), + text = title, + textAlign = TextAlign.Center + ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + text = body, + style = AppTheme.typography.caption1, + textAlign = TextAlign.Start + ) + } + SpacerMedium() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/usecase/BreakSsoTokenUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/usecase/BreakSsoTokenUseCase.kt new file mode 100644 index 00000000..80274713 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/usecase/BreakSsoTokenUseCase.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.debugsettings.usecase + +import de.gematik.ti.erp.app.debugsettings.model.SsoTokenHeader +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.navigation.json +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString +import org.jose4j.base64url.Base64Url +import java.time.Instant +import java.time.temporal.ChronoUnit +import kotlin.time.Duration.Companion.hours + +class BreakSsoTokenUseCase( + private val profileRepository: ProfileRepository, + private val idpRepository: IdpRepository +) { + suspend operator fun invoke( + onResult: (Result) -> Unit + ) { + try { + val profile = profileRepository.activeProfile().first() + idpRepository.authenticationData(profile.id).first().singleSignOnTokenScope?.let { + val newToken = when (it) { + is IdpData.AlternateAuthenticationToken -> + IdpData.AlternateAuthenticationToken( + token = it.token?.breakToken(), + cardAccessNumber = it.cardAccessNumber, + aliasOfSecureElementEntry = it.aliasOfSecureElementEntry, + healthCardCertificate = it.healthCardCertificate.encoded + ) + + is IdpData.DefaultToken -> + IdpData.DefaultToken( + token = it.token?.breakToken(), + cardAccessNumber = it.cardAccessNumber, + healthCardCertificate = it.healthCardCertificate.encoded + ) + + is IdpData.ExternalAuthenticationToken -> + IdpData.ExternalAuthenticationToken( + token = it.token?.breakToken(), + authenticatorName = it.authenticatorName, + authenticatorId = it.authenticatorId + ) + + else -> it + } + idpRepository.saveSingleSignOnToken( + profileId = profile.id, + token = newToken + ) + idpRepository.decryptedAccessToken(profile.id).firstOrNull()?.let { accessToken -> + val updatedAccessToken = accessToken.copy( + accessToken = accessToken.accessToken, + expiresOn = Clock.System.now().minus(12.hours) + ) + idpRepository.saveDecryptedAccessToken( + profileId = profile.id, + accessToken = updatedAccessToken + ) + } + idpRepository.invalidateDecryptedAccessToken(profile.id) + onResult(Result.success(Unit)) + } ?: { + onResult(Result.failure(IllegalStateException("No SSO token found"))) + } + } catch (e: Exception) { + Napier.e { "SSO Token error ${e.message}" } + onResult(Result.failure(e)) + } + } + + // The SSO token does not expire, the signature changes which makes it invalid + @Suppress("MagicNumber") + private fun IdpData.SingleSignOnToken.breakToken(): IdpData.SingleSignOnToken { + val (hours, rest) = token.split('.', limit = 2) + val twelveHoursBefore = Instant.now().minus(12, ChronoUnit.HOURS).epochSecond + val ssoTokenHeaderJsonString = Base64Url.decodeToUtf8String(hours) + val ssoTokenHeader = SsoTokenHeader.toSsoTokenHeader(ssoTokenHeaderJsonString) + val updatedSsoTokenHeader = ssoTokenHeader.copy(exp = twelveHoursBefore) + val updatedSsoTokenHeaderJsonString = json.encodeToString(updatedSsoTokenHeader) + val encodedHeader = Base64Url.encodeUtf8ByteRepresentation(updatedSsoTokenHeaderJsonString) + return IdpData.SingleSignOnToken("$encodedHeader.$rest") + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationControllerModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationControllerModule.kt new file mode 100644 index 00000000..12377e0a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationControllerModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.di + +import android.content.Context +import de.gematik.ti.erp.app.base.NetworkStatusTracker +import de.gematik.ti.erp.app.mainscreen.presentation.AppController +import org.kodein.di.DI +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val applicationControllerModule = DI.Module("applicationControllerModule") { + bindSingleton { AppController(instance(), instance(), instance(), instance(), instance(), instance(), instance()) } + bindSingleton { + val context = instance() + NetworkStatusTracker(context.applicationContext) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt index a89109a9..4c7ef3fc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt index 0f80ec39..8ef7805b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt @@ -1,35 +1,40 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di import de.gematik.ti.erp.app.analytics.di.analyticsModule -import de.gematik.ti.erp.app.appupdate.di.appUpdateModule import de.gematik.ti.erp.app.appsecurity.appSecurityModule +import de.gematik.ti.erp.app.appupdate.di.appUpdateModule import de.gematik.ti.erp.app.authentication.di.authenticationModule import de.gematik.ti.erp.app.cardunlock.di.cardUnlockModule import de.gematik.ti.erp.app.cardwall.cardWallModule +import de.gematik.ti.erp.app.debugsettings.di.debugSettingsModule +import de.gematik.ti.erp.app.featuretoggle.di.newFeaturesSharedPrefsModule import de.gematik.ti.erp.app.idp.idpModule import de.gematik.ti.erp.app.idp.idpUseCaseModule +import de.gematik.ti.erp.app.logger.di.loggerModule +import de.gematik.ti.erp.app.messages.di.messageRepositoryModule +import de.gematik.ti.erp.app.messages.di.messagesModule +import de.gematik.ti.erp.app.medicationplan.di.medicationPlanModule import de.gematik.ti.erp.app.mlkit.mlKitModule -import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule -import de.gematik.ti.erp.app.orders.messageRepositoryModule -import de.gematik.ti.erp.app.orders.messagesModule +import de.gematik.ti.erp.app.onboarding.di.onboardingModule +import de.gematik.ti.erp.app.orderhealthcard.di.orderHealthCardModule import de.gematik.ti.erp.app.pharmacy.di.pharmacyModule import de.gematik.ti.erp.app.pharmacy.di.pharmacyRepositoryModule import de.gematik.ti.erp.app.pkv.consentRepositoryModule @@ -44,18 +49,25 @@ import de.gematik.ti.erp.app.protocol.protocolModule import de.gematik.ti.erp.app.protocol.protocolRepositoryModule import de.gematik.ti.erp.app.redeem.redeemModule import de.gematik.ti.erp.app.settings.settingsModule +import de.gematik.ti.erp.app.settings.settingsRepositoryModule import de.gematik.ti.erp.app.timeouts.di.timeoutsSharedPrefsModule import de.gematik.ti.erp.app.vau.vauModule +import kotlinx.coroutines.Dispatchers import org.kodein.di.DI +import org.kodein.di.bindProvider /** * Use this only in the android-app module */ val featureModule = DI.Module("featureModule", allowSilentOverride = true) { importAll( + applicationControllerModule, + onboardingModule, + dispatchersModule, cardWallModule, appSecurityModule, networkModule, + loggerModule, realmModule, idpModule, idpUseCaseModule, @@ -72,6 +84,16 @@ val featureModule = DI.Module("featureModule", allowSilentOverride = true) { cardUnlockModule, pkvModule, authenticationModule, + // shared-prefs modules + timeoutsSharedPrefsModule, + newFeaturesSharedPrefsModule, + // other modules + analyticsModule, + debugSettingsModule, + mlKitModule, + appUpdateModule, + // repositories + settingsRepositoryModule, profileRepositoryModule, prescriptionRepositoryModule, consentRepositoryModule, @@ -79,10 +101,11 @@ val featureModule = DI.Module("featureModule", allowSilentOverride = true) { pharmacyRepositoryModule, messageRepositoryModule, taskRepositoryModule, - timeoutsSharedPrefsModule, - analyticsModule, - mlKitModule, - appUpdateModule, + medicationPlanModule, allowOverride = true ) } + +val dispatchersModule = DI.Module("dispatchersModule") { + bindProvider { Dispatchers.IO } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt index 689f8c7d..5c0eed99 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di +import com.appmattus.certificatetransparency.certificateTransparencyInterceptor import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.Requirement @@ -30,10 +31,11 @@ import de.gematik.ti.erp.app.interceptor.BearerHeaderInterceptor import de.gematik.ti.erp.app.interceptor.PharmacyRedeemInterceptor import de.gematik.ti.erp.app.interceptor.PharmacySearchInterceptor import de.gematik.ti.erp.app.interceptor.UserAgentHeaderInterceptor +import de.gematik.ti.erp.app.logger.HttpAppLogger +import de.gematik.ti.erp.app.logger.HttpTerminalLogger import de.gematik.ti.erp.app.vau.api.VauService import de.gematik.ti.erp.app.vau.interceptor.VauChannelInterceptor import io.github.aakira.napier.Napier -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.CipherSuite import okhttp3.ConnectionSpec @@ -79,31 +81,23 @@ const val PrefixedLoggerTag = "PrefixedLogger" const val JsonConverterFactoryTag = "JsonConverterFactory" const val JsonFhirConverterFactoryTag = "JsonFhirConverterFactoryTag" +// A_20617-01, @Requirement( - "A_19187", - "A_19739", - "A_19938-01#1", - "A_20033", - "A_20206-01", - "A_20283-01#3", - "A_20529-01", - "A_20606", - "A_20608", - "A_20607", - "A_20609", - "A_20617-01#1", - "A_20618", + "A_20033#1", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Any connection to the IDP or the ERP service uses this configuration." ) @Requirement( - "GS-A_5035", - "GS-A_4387", - "GS-A_4385", - sourceSpecification = "gemSpec_Krypt", + "A_19938-01#1", + "A_19938-01#2", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Any connection to the IDP or the ERP service uses this configuration." ) -@OptIn(ExperimentalSerializationApi::class) +@Requirement( + "A_20623#4", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Any connection to the IDP or the ERP service uses this configuration" +) val networkModule = DI.Module("Network Module") { bindInstance { Json { @@ -116,21 +110,14 @@ val networkModule = DI.Module("Network Module") { instance().asConverterFactory("application/json+fhir".toMediaType()) } @Requirement( - "O.Ntwk_1#2", - "O.Ntwk_2#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Bind the connection specification." + "A_20283-01#4", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Any connection to the IDP or the ERP service uses this configuration." ) @Requirement( - "O.Ntwk_3", + "O.Ntwk_2#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "We use OkHttp for network communication" - ) - @Requirement( - "GS-A_5322", - sourceSpecification = "gemSpec_Krypt", - rationale = "We initialize our okhttp client as singleton. Thus, we support TLS resumption, it is handled by " + - "okhttp. See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-connection/ for more details." + rationale = "Bind the connection specification." ) bindSingleton { OkHttpClient.Builder() @@ -153,15 +140,13 @@ val networkModule = DI.Module("Network Module") { bindSingleton { ApiKeyHeaderInterceptor(instance()) } bindSingleton { BearerHeaderInterceptor(instance()) } - bindProvider { - HttpLoggingInterceptor(NapierLogger()).also { - it.setLevel(HttpLoggingInterceptor.Level.BODY) - } - } + // Two loggers are used to log the HTTP requests and responses. One is used for the terminal and the other for the app. + bindProvider { HttpLoggingInterceptor(HttpTerminalLogger()).also { it.setLevel(HttpLoggingInterceptor.Level.BODY) } } + bindProvider { HttpAppLogger(instance()) } bindMultiton, Interceptor>(PrefixedLoggerTag) { (tagSuffix, withBody) -> AuditEventFilteredHttpLoggingInterceptor( - HttpLoggingInterceptor(NapierLogger(tagSuffix)).also { + HttpLoggingInterceptor(HttpTerminalLogger(tagSuffix)).also { if (BuildKonfig.INTERNAL) { if (withBody) { it.setLevel(HttpLoggingInterceptor.Level.BODY) @@ -173,18 +158,26 @@ val networkModule = DI.Module("Network Module") { ) } + @Requirement( + "O.Ntwk_3#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "We use OkHttp with different interceptors to make it more secure." + ) // IDP Service bindSingleton { val clientBuilder = instance().newBuilder() val userAgentInterceptor = instance() val endpointHelper = instance() val apiKeyInterceptor = instance() - val loggingInterceptor = instance() + val httpTerminalLogger = instance() + val httpAppLogger = instance() val client = clientBuilder .addInterceptor(userAgentInterceptor) .addInterceptor(apiKeyInterceptor) - .addInterceptor(loggingInterceptor) + .addCertificateTransparencyInterceptor() + .addInterceptor(httpTerminalLogger) + .addInterceptor(httpAppLogger) .followRedirects(false) .build() @@ -197,6 +190,11 @@ val networkModule = DI.Module("Network Module") { .create(IdpService::class.java) } + @Requirement( + "O.Ntwk_3#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "We use OkHttp with different interceptors to make it more secure." + ) // ERP Service bindSingleton { val clientBuilder = instance().newBuilder() @@ -209,6 +207,7 @@ val networkModule = DI.Module("Network Module") { instance, Interceptor>(PrefixedLoggerTag, "[inner request]" to true) val outerLoggingInterceptor = instance, Interceptor>(PrefixedLoggerTag, "[outer request]" to false) + val httpAppLogger = instance() clientBuilder.cache(null) @@ -222,8 +221,12 @@ val networkModule = DI.Module("Network Module") { clientBuilder.addInterceptor(userAgentInterceptor) clientBuilder.addInterceptor(apiKeyInterceptor) + clientBuilder.addCertificateTransparencyInterceptor() + clientBuilder.addInterceptor(outerLoggingInterceptor) + clientBuilder.addInterceptor(httpAppLogger) + Retrofit.Builder() .client(clientBuilder.build()) .baseUrl(endpointHelper.eRezeptServiceUri) @@ -233,17 +236,25 @@ val networkModule = DI.Module("Network Module") { .create(ErpService::class.java) } + @Requirement( + "O.Ntwk_3#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "We use OkHttp with different interceptors to make it more secure." + ) // The VAU service is only used to get CertList & OCSPList and NOT to post to the VAU endpoint bindSingleton { val clientBuilder = instance().newBuilder() val userAgentInterceptor = instance() val apiKeyInterceptor = instance() val endpointHelper = instance() - val loggingInterceptor = instance() + val httpAppLogger = instance() + val httpTerminalLogger = instance() clientBuilder.addInterceptor(apiKeyInterceptor) clientBuilder.addInterceptor(userAgentInterceptor) - clientBuilder.addInterceptor(loggingInterceptor) + clientBuilder.addCertificateTransparencyInterceptor() + clientBuilder.addInterceptor(httpTerminalLogger) + clientBuilder.addInterceptor(httpAppLogger) Retrofit.Builder() .client(clientBuilder.build()) @@ -253,13 +264,23 @@ val networkModule = DI.Module("Network Module") { .create(VauService::class.java) } + @Requirement( + "O.Ntwk_3#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "We use OkHttp with different interceptors to make it more secure." + ) // Pharmacy Redeem Service bindSingleton { val clientBuilder = instance().newBuilder() - val loggingInterceptor = instance() + val httpAppLogger = instance() + val httpTerminalLogger = instance() + val userAgentInterceptor = instance() clientBuilder - .addInterceptor(loggingInterceptor) + .addInterceptor(userAgentInterceptor) + .addCertificateTransparencyInterceptor() + .addInterceptor(httpTerminalLogger) + .addInterceptor(httpAppLogger) if (BuildKonfig.INTERNAL) { clientBuilder.addInterceptor(PharmacyRedeemInterceptor()) @@ -272,15 +293,25 @@ val networkModule = DI.Module("Network Module") { .create(PharmacyRedeemService::class.java) } + @Requirement( + "O.Ntwk_3#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "We use OkHttp with different interceptors to make it more secure." + ) // Pharmacy Search Service bindSingleton { val clientBuilder = instance().newBuilder() val endpointHelper = instance() - val loggingInterceptor = instance() + val httpAppLogger = instance() + val httpTerminalLogger = instance() + val userAgentInterceptor = instance() clientBuilder .addInterceptor(PharmacySearchInterceptor(instance())) - .addInterceptor(loggingInterceptor) + .addInterceptor(userAgentInterceptor) + .addCertificateTransparencyInterceptor() + .addInterceptor(httpTerminalLogger) + .addInterceptor(httpAppLogger) Retrofit.Builder() .client(clientBuilder.build()) @@ -292,33 +323,26 @@ val networkModule = DI.Module("Network Module") { } @Requirement( - "A_20206-01", - "A_17322", - "A_18464", - "A_18467", - "A_21332", - "A_21275-01", - sourceSpecification = "gemSpec_eRp_FdV", + "A_21332-02#1", + sourceSpecification = "gemSpec_Krypt", rationale = "Any connection initiated by the app uses TLS 1.2 or higher." ) @Requirement( - "GS-A_4357-2#1", - "GS-A_4361-2#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Cipher Suites regarding sha256WithRsaEncryption are listed below, see RSA specific cipher suites." -) -@Requirement( - "O.Ntwk_1#1", "O.Ntwk_2#1", sourceSpecification = "BSI-eRp-ePA", rationale = "Any connection initiated by the app uses TLS 1.2 or higher." ) @Requirement( - "GS-A_5035#1", - "GS-A_4385#1", - "GS-A_4387#1", - sourceSpecification = "gemSpec_Krypt", - rationale = "Any connection initiated by the app uses TLS 1.2 or higher." + "O.Auth_12#2", + "O.Resi_6#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The TLS specification is done here and using RESTRICTED_TLS. We inherently support tlsExtensions." +) +@Requirement( + "A_20606#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Add TLS versioning through connectionSpecs.", + codeLines = 6 ) private fun getConnectionSpec(): List = ConnectionSpec .Builder(ConnectionSpec.RESTRICTED_TLS) @@ -334,10 +358,16 @@ private fun getConnectionSpec(): List = ConnectionSpec CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // TLS 1.3 CipherSuite.TLS_AES_128_GCM_SHA256, - CipherSuite.TLS_AES_256_GCM_SHA384, - CipherSuite.TLS_CHACHA20_POLY1305_SHA256 + CipherSuite.TLS_AES_256_GCM_SHA384 ) .build() .let { listOf(it) } + +private fun OkHttpClient.Builder.addCertificateTransparencyInterceptor() = + addNetworkInterceptor( + certificateTransparencyInterceptor { + failOnError = true + } + ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt index 1d0dc721..de2c637e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt @@ -1,37 +1,36 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di import android.content.Context import android.content.SharedPreferences - import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey - import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.UncaughtException import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.db.appSchemas import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 import de.gematik.ti.erp.app.db.openRealmWith import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.secureRandomInstance +import io.realm.kotlin.exceptions.RealmException import org.jose4j.base64url.Base64 import org.kodein.di.DI import org.kodein.di.bindEagerSingleton @@ -46,37 +45,34 @@ private const val PassphraseSizeInBytes = 64 const val RealmDatabaseSecurePreferencesTag = "RealmDatabaseSecurePreferences" -@Requirement( - "A_19186", - "A_20184#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Only encrypted SharedPreferences and an encrypted database are used to store data. " + - "It is not possible to create backups from within the app. If this is triggered externally, " + - "we use an encrypted database and encrypted preferences. The access keys are stored in the encrypted " + - "shared preferences" -) -@Requirement( - "O.Arch_2#1", - "O.Arch_4#1", - "O.Data_2", - "O.Data_3", - "O.Data_14", - "O.Data_15#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Only encrypted SharedPreferences and an encrypted database are used to store data. " + - "It is not possible to create backups from within the app. If this is triggered externally, " + - "we use an encrypted database and encrypted preferences. The access keys are stored in the encrypted " + - "shared preferences" -) -@Requirement( - "O.Source_2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Database access is done via prepared statements featured by Realm Database." -) +// TODO: Does not get printed in technical requirements val realmModule = DI.Module("realmModule") { + @Requirement( + "O.Arch_4#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Implementation of data storage" + ) + @Requirement( + "O.Data_14#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "All data is encrypted irrespective of the data type and lock state." + ) + @Requirement( + "O.Data_14#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "All data is encrypted irrespective of the data type and lock state." + ) bindSingleton(RealmDatabaseSecurePreferencesTag) { val context = instance() + @Requirement( + "O.Arch_2#1", + "O.Data_2#1", + "O.Data_3#1", + "O.Purp_8#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Data storage using EncryptedSharedPreferences." + ) EncryptedSharedPreferences.create( context, ENCRYPTED_REALM_PREFS_FILE_NAME, @@ -89,9 +85,20 @@ val realmModule = DI.Module("realmModule") { bindEagerSingleton { val securePrefs = instance(RealmDatabaseSecurePreferencesTag) + @Requirement( + "O.Arch_2#2", + "O.Arch_4#1", + "O.Data_2#4", + "O.Data_3#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Database configuration with encryption key. " + + "The key is stored in the encrypted shared preferences." + ) try { + val context = instance() + val profileName = context.resources.getString(R.string.onboarding_default_profile_name) openRealmWith( - schemas = appSchemas, + schemas = appSchemas(profileName = profileName), configuration = { it.encryptionKey(Base64.decode(getPassphrase(securePrefs))) } @@ -104,7 +111,7 @@ val realmModule = DI.Module("realmModule") { } } } catch (expected: Throwable) { - throw UncaughtException(expected) + throw RealmException("exception on module start", expected) } } } @@ -126,7 +133,7 @@ private fun generatePassPhrase(): String { } @Requirement( - "O.Data_4", + "O.Data_4#1", sourceSpecification = "BSI-eRp-ePA", rationale = "Database access is done via prepared statements featured by Realm Database." ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt index ed5b3016..c3339525 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.featuretoggle @@ -23,23 +23,31 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map private val Context.dataStore by preferencesDataStore("featureToggles") enum class Features(val featureName: String) { // REDEEM_WITHOUT_TI("RedeemWithoutTI"), + MEDICATION_PLAN("MedicationPlan"), } -// TODO: Poor naming, change it class FeatureToggleManager(val context: Context) { private val dataStore = context.dataStore - val features = Features.values() + val features = Features.entries.toTypedArray() - fun isFeatureEnabled(featureName: String) = dataStore.data.map { prefs -> - prefs[booleanPreferencesKey(featureName)] ?: false - } + fun isFeatureEnabled(featureName: Features): Flow = + if (BuildConfigExtension.isInternalDebug) { + dataStore.data.map { prefs -> + prefs[booleanPreferencesKey(featureName.featureName)] ?: false + } + } else { + flowOf(false) // hide features in release build + } fun featuresState() = dataStore.data.map { prefs -> val map: MutableMap = mutableMapOf() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/datasource/NewFeaturesLocalDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/datasource/NewFeaturesLocalDataSource.kt new file mode 100644 index 00000000..82a45c6f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/datasource/NewFeaturesLocalDataSource.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.datasource + +import android.content.SharedPreferences +import androidx.core.content.edit +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature +import io.github.aakira.napier.Napier + +class NewFeaturesLocalDataSource( + private val sharedPreferences: SharedPreferences +) { + fun setDefaults() { + try { + sharedPreferences.edit { + NewFeature.entries.forEach { feature -> + putBoolean(feature.name, feature.default) + apply() + } + } + } catch (e: Throwable) { + Napier.e { "error on NewFeaturesLocalDataSource defaults ${e.message}" } + } + } + + fun isNewFeatureSeen(feature: NewFeature): Boolean { + return sharedPreferences.getBoolean(feature.name, false) + } + + fun markFeatureSeen(feature: NewFeature) { + sharedPreferences.edit { + putBoolean(feature.name, true) + apply() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/di/NewFeaturesSharedPrefsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/di/NewFeaturesSharedPrefsModule.kt new file mode 100644 index 00000000..c6255397 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/di/NewFeaturesSharedPrefsModule.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.di + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.featuretoggle.datasource.NewFeaturesLocalDataSource +import de.gematik.ti.erp.app.featuretoggle.repository.DefaultNewFeaturesRepository +import de.gematik.ti.erp.app.featuretoggle.repository.NewFeaturesRepository +import de.gematik.ti.erp.app.featuretoggle.usecase.IsNewFeatureSeenUseCase +import de.gematik.ti.erp.app.featuretoggle.usecase.MarkNewFeatureSeenUseCase +import de.gematik.ti.erp.app.featuretoggle.usecase.SetNewFeatureDefaultsUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +private const val ENCRYPTED_PREFS_FILE_NAME = "new_features_encrypted_shared_prefs" +private const val ENCRYPTED_PREFS_MASTER_KEY_ALIAS = "NEW_FEATURES_ENCRYPTED_PREFS_MASTER_KEY_ALIAS" + +private const val ENCRYPTED_SHARED_PREFS_TAG = "NewFeaturesEncryptedSharedPrefsTag" + +val newFeaturesSharedPrefsModule = DI.Module("newFeaturesSharedPrefsModule") { + // encrypted key for the shared preferences + bindSingleton(ENCRYPTED_PREFS_MASTER_KEY_ALIAS) { + val context = instance() + val masterKey = MasterKey.Builder(context, ENCRYPTED_PREFS_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + masterKey + } + // shared preferences + bindSingleton(ENCRYPTED_SHARED_PREFS_TAG) { + val context = instance() + val masterKey = instance(ENCRYPTED_PREFS_MASTER_KEY_ALIAS) + + EncryptedSharedPreferences.create( + context, + ENCRYPTED_PREFS_FILE_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + // data source + bindProvider { + val sharedPreferences = instance(ENCRYPTED_SHARED_PREFS_TAG) + NewFeaturesLocalDataSource(sharedPreferences) + } + // repository + bindProvider { + DefaultNewFeaturesRepository(instance()) + } + // use-cases + bindProvider { IsNewFeatureSeenUseCase(instance()) } + bindProvider { MarkNewFeatureSeenUseCase(instance()) } + bindProvider { + SetNewFeatureDefaultsUseCase(instance()).apply { CoroutineScope(Dispatchers.IO).launch { invoke() } } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/model/NewFeature.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/model/NewFeature.kt new file mode 100644 index 00000000..8d52a115 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/model/NewFeature.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.model + +import androidx.annotation.StringRes +import de.gematik.ti.erp.app.features.R + +// if the feature is not to be shown by default, set the default to false +enum class NewFeature( + val default: Boolean, + @StringRes val title: Int, + @StringRes val description: Int +) { + ORDERS_SCREEN_NO_PROFILE_BAR( + default = true, + title = R.string.new_feature_orders_screen_no_profile_bar_title, + description = R.string.new_feature_orders_screen_no_profile_bar_description + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/repository/NewFeaturesRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/repository/NewFeaturesRepository.kt new file mode 100644 index 00000000..80e31c0d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/repository/NewFeaturesRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.repository + +import de.gematik.ti.erp.app.featuretoggle.datasource.NewFeaturesLocalDataSource +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature + +interface NewFeaturesRepository { + fun setDefaults() + fun isNewFeatureSeen(feature: NewFeature): Boolean + fun markFeatureSeen(feature: NewFeature) +} + +class DefaultNewFeaturesRepository( + private val localDataSource: NewFeaturesLocalDataSource +) : NewFeaturesRepository { + override fun setDefaults() { + localDataSource.setDefaults() + } + + override fun isNewFeatureSeen(feature: NewFeature): Boolean = localDataSource.isNewFeatureSeen(feature) + + override fun markFeatureSeen(feature: NewFeature) { + localDataSource.markFeatureSeen(feature) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/IsNewFeatureSeenUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/IsNewFeatureSeenUseCase.kt new file mode 100644 index 00000000..0fc38c60 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/IsNewFeatureSeenUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.usecase + +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature +import de.gematik.ti.erp.app.featuretoggle.repository.NewFeaturesRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class IsNewFeatureSeenUseCase( + private val repository: NewFeaturesRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(feature: NewFeature): Boolean = + withContext(dispatcher) { + return@withContext repository.isNewFeatureSeen(feature) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/MarkNewFeatureSeenUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/MarkNewFeatureSeenUseCase.kt new file mode 100644 index 00000000..f74560fa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/MarkNewFeatureSeenUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.usecase + +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature +import de.gematik.ti.erp.app.featuretoggle.repository.NewFeaturesRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MarkNewFeatureSeenUseCase( + private val repository: NewFeaturesRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(feature: NewFeature) { + withContext(dispatcher) { + repository.markFeatureSeen(feature) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/SetNewFeatureDefaultsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/SetNewFeatureDefaultsUseCase.kt new file mode 100644 index 00000000..4e705f41 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/usecase/SetNewFeatureDefaultsUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.featuretoggle.usecase + +import de.gematik.ti.erp.app.featuretoggle.repository.NewFeaturesRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SetNewFeatureDefaultsUseCase( + private val repository: NewFeaturesRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke() { + withContext(dispatcher) { + repository.setDefaults() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/DomainsNotVerifiedDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/DomainsNotVerifiedDialog.kt deleted file mode 100644 index 9d3b939d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/DomainsNotVerifiedDialog.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.gid.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog -import de.gematik.ti.erp.app.utils.compose.ErezeptText -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme - -@Composable -fun DomainsNotVerifiedDialog( - onClickSettingsOpen: () -> Unit, - onDismissRequest: () -> Unit -) { - ErezeptAlertDialog( - modifier = Modifier.width(600.dp), - title = stringResource(R.string.domain_verifier_dialog_title), - body = stringResource(R.string.domain_verifier_dialog_body), - confirmText = stringResource(R.string.domain_verifier_dialog_button), - dismissText = stringResource(R.string.cancel), - titleAlignment = ErezeptText.ErezeptTextAlignment.Center, - buttonsArrangement = Arrangement.Start, - onDismissRequest = onDismissRequest, - onConfirmRequest = onClickSettingsOpen - ) -} - -@LightDarkPreview -@Composable -fun DomainsNotVerifiedDialogPreview() { - PreviewAppTheme { - DomainsNotVerifiedDialog( - onClickSettingsOpen = {}, - onDismissRequest = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/components/DomainsNotVerifiedDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/components/DomainsNotVerifiedDialog.kt new file mode 100644 index 00000000..6c8d90d6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/gid/ui/components/DomainsNotVerifiedDialog.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.gid.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErezeptText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun DomainsNotVerifiedDialog( + onClickSettingsOpen: () -> Unit, + onDismissRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.domain_verifier_dialog_title), + bodyText = stringResource(R.string.domain_verifier_dialog_body), + confirmText = stringResource(R.string.domain_verifier_dialog_button), + dismissText = stringResource(R.string.cancel), + titleAlignment = ErezeptText.TextAlignment.Center, + buttonsArrangement = Arrangement.Start, + onDismissRequest = onDismissRequest, + onConfirmRequest = onClickSettingsOpen + ) +} + +@LightDarkPreview +@Composable +fun DomainsNotVerifiedDialogPreview() { + PreviewAppTheme { + DomainsNotVerifiedDialog( + onClickSettingsOpen = {}, + onDismissRequest = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt index 023d2e9d..4c0f32e1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt @@ -1,30 +1,32 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.idp.repository.AccessTokenDataSource +import de.gematik.ti.erp.app.idp.repository.DefaultIdpRepository import de.gematik.ti.erp.app.idp.repository.IdpLocalDataSource import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository import de.gematik.ti.erp.app.idp.repository.IdpRemoteDataSource import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.idp.usecase.AuthenticateWithExternalHealthInsuranceAppUseCase +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase import de.gematik.ti.erp.app.idp.usecase.DefaultIdpUseCase import de.gematik.ti.erp.app.idp.usecase.GetHealthInsuranceAppIdpsUseCase import de.gematik.ti.erp.app.idp.usecase.GetUniversalLinkForHealthInsuranceAppsUseCase @@ -59,7 +61,7 @@ val idpModule = DI.Module("idpModule") { } } bindSingleton { AccessTokenDataSource() } - bindProvider { IdpRepository(instance(), instance(), instance()) } + bindProvider { DefaultIdpRepository(instance(), instance(), instance()) } } val idpUseCaseModule = DI.Module("idpUseCaseModule", allowSilentOverride = true) { @@ -90,4 +92,5 @@ val idpUseCaseModule = DI.Module("idpUseCaseModule", allowSilentOverride = true) ) } bindProvider { RemoveAuthenticationUseCase(instance()) } + bindProvider { ChooseAuthenticationDataUseCase(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt index 230945a3..b8d57024 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.info @@ -30,4 +30,5 @@ interface BuildConfigInformation { @Composable fun inDarkTheme(): String fun nfcInformation(context: Context): String + fun isMockedApp(): Boolean } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt index 7b5c6455..21584813 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file: Suppress("TopLevelPropertyNaming") + package de.gematik.ti.erp.app.interceptor import de.gematik.ti.erp.app.BuildKonfig @@ -34,11 +36,15 @@ private const val invalidAccessTokenHeader = "Www-Authenticate" private const val invalidAccessTokenValue = "Bearer realm='prescriptionserver.telematik', error='invalACCESS_TOKEN'" @Requirement( - "A_19187", - "A_20529-01", - sourceSpecification = "gemSpec_eRp_FdV", + "A_20529-01#01", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Any connection initiated by the app uses TLS 1.2 or higher." ) +@Requirement( + "A_20602#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "The interceptor pattern is used to add the bearer token to the request." +) class BearerHeaderInterceptor( private val idpUseCase: IdpUseCase ) : Interceptor { @@ -77,6 +83,7 @@ class PharmacySearchInterceptor(private val endpointHelper: EndpointHelper) : In override fun intercept(chain: Interceptor.Chain): Response { val original: Request = chain.request() val request: Request = original.newBuilder() + .header("X-Api-Key", endpointHelper.getPharmacyApiKey()) .build() return chain.proceed(request) } @@ -94,7 +101,7 @@ class PharmacyRedeemInterceptor : Interceptor { class UserAgentHeaderInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() - .header("User-Agent", BuildKonfig.USER_AGENT) + .header("User-Agent", "${BuildKonfig.USER_AGENT}/${BuildKonfig.CLIENT_ID}") .build() return chain.proceed(request) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpAppLogger.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpAppLogger.kt new file mode 100644 index 00000000..553f617f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpAppLogger.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger + +import de.gematik.ti.erp.app.logger.model.ContentLog +import de.gematik.ti.erp.app.logger.model.HeaderLog +import de.gematik.ti.erp.app.logger.model.LogEntry +import de.gematik.ti.erp.app.logger.model.RequestLog +import de.gematik.ti.erp.app.logger.model.ResponseLog +import de.gematik.ti.erp.app.logger.model.TimingsLog +import io.github.aakira.napier.Napier +import kotlinx.datetime.Clock +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.IOException + +class HttpAppLogger( + private val sessionLog: SessionLogHolder +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val startTime = System.nanoTime() + + val response: Response = try { + chain.proceed(request) + } catch (e: IOException) { + Napier.e { "error on HttpAppLogger ${e.stackTraceToString()}" } + throw e + } + + val endTime = System.nanoTime() + + // Peek the response body without consuming it since some response are only one time read + val responseBody = response.peekBody(Long.MAX_VALUE) + + val logEntry = LogEntry( + timestamp = Clock.System.now().toString(), + request = RequestLog( + method = request.method, + url = request.url.toString(), + headers = request.headers.map { header -> + HeaderLog( + name = header.first, + value = header.second + ) + } + ), + response = ResponseLog( + status = response.code, + statusText = response.message, + headers = response.headers.map { header -> + HeaderLog( + name = header.first, + value = header.second + ) + }, + content = ContentLog( + mimeType = response.body?.contentType()?.type ?: "", + text = response.body?.string().orEmpty() + ) + ), + timings = TimingsLog( + send = startTime, + wait = endTime - startTime, + receive = System.nanoTime() - endTime + ) + ) + + // add this to the logs + sessionLog.http.value.add(logEntry) + + // return the recreated response with the peeked body to ensure it is still available for further consumption + return response.newBuilder() + .body(responseBody.bytes().toResponseBody(responseBody.contentType())) + .build() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpTerminalLogger.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpTerminalLogger.kt new file mode 100644 index 00000000..b6e06343 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/HttpTerminalLogger.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger + +import io.github.aakira.napier.Napier +import okhttp3.logging.HttpLoggingInterceptor + +class HttpTerminalLogger( + tagSuffix: String? = null +) : HttpLoggingInterceptor.Logger { + private val tag = if (tagSuffix != null) { + "OkHttp $tagSuffix" + } else { + "OkHttp" + } + + override fun log(message: String) { + Napier.d(message, tag = tag) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/SessionLogHolder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/SessionLogHolder.kt new file mode 100644 index 00000000..1aa298a3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/SessionLogHolder.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger + +import de.gematik.ti.erp.app.logger.model.LogEntry +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class SessionLogHolder { + val http: StateFlow> = MutableStateFlow(mutableListOf()) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/di/LoggerModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/di/LoggerModule.kt new file mode 100644 index 00000000..484ea245 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/di/LoggerModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger.di + +import de.gematik.ti.erp.app.logger.SessionLogHolder +import org.kodein.di.DI +import org.kodein.di.bindSingleton + +val loggerModule = DI.Module("Logger Module") { + bindSingleton { SessionLogHolder() } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/mapper/LogEntryToHar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/mapper/LogEntryToHar.kt new file mode 100644 index 00000000..afd9525e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/mapper/LogEntryToHar.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger.mapper + +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.logger.model.Cache +import de.gematik.ti.erp.app.logger.model.Content +import de.gematik.ti.erp.app.logger.model.Creator +import de.gematik.ti.erp.app.logger.model.Entry +import de.gematik.ti.erp.app.logger.model.Har +import de.gematik.ti.erp.app.logger.model.Header +import de.gematik.ti.erp.app.logger.model.Log +import de.gematik.ti.erp.app.logger.model.LogEntry +import de.gematik.ti.erp.app.logger.model.Request +import de.gematik.ti.erp.app.logger.model.Response +import de.gematik.ti.erp.app.logger.model.Timings +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun List.toHar() = Har( + log = Log( + version = "1.2", + creator = Creator( + name = "ErpApp Android Logger", + version = BuildKonfig.VERSION_NAME + ), + pages = emptyList(), + entries = this.toHarEntries() + ) +) + +private fun List.toHarEntries(): List = + this.map { logEntry -> + Entry( + startedDateTime = logEntry.timestamp, + time = logEntry.timings.send + logEntry.timings.wait + logEntry.timings.receive, + request = Request( + httpVersion = "HTTP/1.1", + method = logEntry.request.method, + url = logEntry.request.url, + headers = logEntry.request.headers.map { header -> + Header( + name = header.name, + value = header.value + ) + }, + queryString = emptyList() + ), + response = Response( + httpVersion = "HTTP/1.1", + status = logEntry.response.status, + statusText = logEntry.response.statusText, + headers = logEntry.response.headers.map { header -> + Header( + name = header.name, + value = header.value + ) + }, + content = Content( + size = logEntry.response.content.text.length.toLong(), + mimeType = logEntry.response.content.mimeType, + text = logEntry.response.content.text + ) + ), + cache = Cache(), + timings = Timings( + send = logEntry.timings.send, + wait = logEntry.timings.wait, + receive = logEntry.timings.receive + ) + ) + } + +private val json = Json { prettyPrint = true } + +fun Har.toJson() = json.encodeToString(this) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/HarModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/HarModels.kt new file mode 100644 index 00000000..da1292b1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/HarModels.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.logger.model + +import kotlinx.serialization.Serializable + +// Har models for the HAR format (HTTP Archive) + +@Serializable +data class Har( + val log: Log +) + +@Serializable +data class Log( + val version: String, + val creator: Creator, + val pages: List, + val entries: List +) + +@Serializable +data class Creator( + val name: String, + val version: String +) + +@Serializable +data class Page( + val startedDateTime: String, + val id: String, + val title: String, + val pageTimings: PageTimings +) + +@Serializable +data class PageTimings( + val onContentLoad: Long? = null, + val onLoad: Long? = null +) + +@Serializable +data class Entry( + val startedDateTime: String, + val time: Long, + val request: Request, + val response: Response, + val cache: Cache, + val timings: Timings +) + +@Serializable +data class Request( + val method: String, + val url: String, + val httpVersion: String, + val headers: List
, + val queryString: List, + val headersSize: Long = -1, + val bodySize: Long = -1 +) + +@Serializable +data class Response( + val status: Int, + val statusText: String, + val httpVersion: String, + val headers: List
, + val content: Content, + val redirectURL: String = "", + val headersSize: Long = -1, + val bodySize: Long = -1 +) + +@Serializable +data class Header( + val name: String, + val value: String +) + +@Serializable +data class QueryString( + val name: String, + val value: String +) + +@Serializable +data class Content( + val size: Long, + val mimeType: String, + val text: String? = null, + val encoding: String? = null +) + +@Serializable +data class Cache( + val beforeRequest: CacheDetail? = null, + val afterRequest: CacheDetail? = null +) + +@Serializable +data class CacheDetail( + val expires: String? = null, + val lastAccess: String? = null, + val eTag: String? = null, + val hitCount: Int? = null +) + +@Serializable +data class Timings( + val send: Long, + val wait: Long, + val receive: Long, + val dns: Long? = null, + val connect: Long? = null, + val ssl: Long? = null +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/LogEntryModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/LogEntryModels.kt new file mode 100644 index 00000000..4286a962 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/logger/model/LogEntryModels.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.logger.model + +import kotlinx.serialization.Serializable + +@Serializable +data class LogEntry( + val timestamp: String, + val request: RequestLog, + val response: ResponseLog, + val timings: TimingsLog +) + +@Serializable +data class RequestLog( + val method: String, + val url: String, + val headers: List +) + +@Serializable +data class ResponseLog( + val status: Int, + val statusText: String, + val headers: List, + val content: ContentLog +) + +@Serializable +data class HeaderLog( + val name: String, + val value: String +) + +@Serializable +data class ContentLog( + val mimeType: String, + val text: String +) + +@Serializable +data class TimingsLog( + val send: Long, + val wait: Long, + val receive: Long +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/EditProfilePictureSelectionState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/EditProfilePictureSelectionState.kt new file mode 100644 index 00000000..c750cb0f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/EditProfilePictureSelectionState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.model + +enum class EditProfilePictureSelectionState { + Emoji, Camera, PersonalizedImage +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/MultiProfileAppBarFlowWrapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/MultiProfileAppBarFlowWrapper.kt new file mode 100644 index 00000000..ce45afff --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/model/MultiProfileAppBarFlowWrapper.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.model + +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.flow.StateFlow + +data class MultiProfileAppBarFlowWrapper( + val activeProfile: StateFlow, + val existingProfiles: StateFlow>, + val isProfileRefreshing: StateFlow +) { + companion object { + val DEFAULT_EMPTY_PROFILE = Profile( + id = "", + name = "", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), + isActive = false, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = null, + ssoTokenScope = null, + avatar = ProfilesData.Avatar.PersonalizedImage + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt deleted file mode 100644 index 70e8c023..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.navigation - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.orders.ui.OrderScreen -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.prescription.ui.PrescriptionsScreen -import de.gematik.ti.erp.app.prescription.presentation.rememberPrescriptionsController -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.settings.navigation.settingsGraph - -@Composable -fun MainScreenContentNavHost( - modifier: Modifier, - mainScreenController: MainScreenController, - mainNavController: NavController, - bottomNavController: NavHostController, - onElevateTopBar: (Boolean) -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit -) { - Box( - modifier = modifier - .testTag("main_screen") - ) { - NavHost( - bottomNavController, - startDestination = MainNavigationScreens.Prescriptions.path() - ) { - composable(MainNavigationScreens.Prescriptions.route) { - val prescriptionsController = rememberPrescriptionsController() - val profilesController = rememberProfileController() - val activeProfile by profilesController.getActiveProfileState() - PrescriptionsScreen( - controller = prescriptionsController, - mainScreenController = mainScreenController, - activeProfile = activeProfile, - onElevateTopBar = onElevateTopBar, - onClickArchive = onClickArchive, - onClickAvatar = onClickAvatar, - onShowCardWall = { - mainNavController.navigate( - CardWallRoutes.CardWallIntroScreen.path(activeProfile.id) - ) - }, - onClickPrescription = { taskId -> - mainNavController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailScreen.path( - taskId = taskId - ) - ) - } - ) - } - composable(MainNavigationScreens.Orders.route) { - OrderScreen( - mainNavController = mainNavController, - mainScreenController = mainScreenController, - onElevateTopBar = onElevateTopBar - ) - } - // duplicate exists because of 2 main nav-hosts - settingsGraph(navController = mainNavController) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt deleted file mode 100644 index 89e1f2a0..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt +++ /dev/null @@ -1,600 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import com.google.accompanist.navigation.material.BottomSheetNavigator -import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi -import com.google.accompanist.navigation.material.ModalBottomSheetLayout -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.navigation.trackingGraph -import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync -import de.gematik.ti.erp.app.analytics.trackPopUps -import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes -import de.gematik.ti.erp.app.appsecurity.navigation.appSecurityGraph -import de.gematik.ti.erp.app.cardunlock.navigation.cardUnlockGraph -import de.gematik.ti.erp.app.cardwall.navigation.cardWallGraph -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.core.LocalDi -import de.gematik.ti.erp.app.debugsettings.navigation.exampleScreensGraph -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenScaffoldContainer -import de.gematik.ti.erp.app.mlkit.navigation.MlKitRoutes -import de.gematik.ti.erp.app.mlkit.navigation.mlKitGraph -import de.gematik.ti.erp.app.navigation.navigateAndClearStack -import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes -import de.gematik.ti.erp.app.onboarding.navigation.onboardingGraph -import de.gematik.ti.erp.app.onboarding.ui.DeprecatedOnboardingNavigationScreens -import de.gematik.ti.erp.app.orderhealthcard.ui.HealthCardContactOrderScreen -import de.gematik.ti.erp.app.orders.ui.OrderOverviewScreen -import de.gematik.ti.erp.app.pharmacy.navigation.pharmacyGraph -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyNavigation -import de.gematik.ti.erp.app.pkv.navigation.pkvGraph -import de.gematik.ti.erp.app.prescription.detail.navigation.prescriptionDetailGraph -import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes -import de.gematik.ti.erp.app.prescription.navigation.prescriptionGraph -import de.gematik.ti.erp.app.prescription.presentation.rememberPrescriptionsController -import de.gematik.ti.erp.app.prescription.ui.ArchiveScreen -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.ui.RefreshedState -import de.gematik.ti.erp.app.profiles.navigation.profileGraph -import de.gematik.ti.erp.app.redeem.ui.RedeemNavigation -import de.gematik.ti.erp.app.settings.navigation.settingsGraph -import de.gematik.ti.erp.app.troubleshooting.navigation.troubleShootingGraph -import de.gematik.ti.erp.app.ui.DebugScreenWrapper -import de.gematik.ti.erp.app.utils.compose.NavigationAnimation -import de.gematik.ti.erp.app.utils.compose.navigationModeState - -@OptIn(ExperimentalMaterialNavigationApi::class) -@Requirement( - "A_19178", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "see documentation-internal/security/mstg.adoc" -) -@Requirement( - "A_19983", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "All used services except our analytics framework are permitted and attested by the Gematik and " + - "under the TI monitoring. The usage of our analytics framework is not under our control, but we " + - "exclusively send data to it and receive none." -) -@Requirement( - "A_19979", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "We use external services: The Apothekenverzeichnis and our analytics framework. " + - "During the communication with the pharmacy, there will be data shared via a prescription code. " + - "The requirement to this feature is described in gemSpec_eRp_FdV section 5.2.3.10 and 5.2.3.11." + - "Our analytics framework does not use medical personal data, see DSFA section 5.6 " + - "Verarbeitungsvorgang 4: Rezepte einlösen" -) -@Requirement( - "A_19182", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "In order to minimize the risk of unknown vulnerabilities in dependencies, we use different measures:" + - "- We develop according to Security by Design Principles (see E-Rezept-App - SSDLC.pdf - Section " + - "Richtlinien, Vorgaben und Best Practices)" + - "- We train our engineers focussing on secure design and coding best practices " + - "(see Sicherheitsschulungen.pdf)" + - "- We publish our Code on Github and use a bug bounty program (https://www.gematik.de/datensicherheit -> " + - "Coordinated Vulnerability Disclosure Program)" -) -@Requirement( - "GS-A_5526", - sourceSpecification = "gemSpec_Krypt", - rationale = "Renegotiation is disabled by default in BoringSSL:\n" + - "'https://boringssl.googlesource.com/boringssl/+/9f69f139ed1088daabb6525f0c9c34d1e89688f7/PORTING.md" + - "#:~:text=TLS%20renegotiation&text=Renegotiation%20is%20an%20extremely%20problematic," + - "rejects%20peer%20renegotiations%20by%20default.'" -) -@Requirement( - "O.Arch_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We implement an SSDLC" -) -@Requirement( - "O.Arch_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "All cryptography is specified by gemSpec_Krypt in corporation with BSI" -) -@Requirement( - "O.Arch_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Authentication and Authorization are not handled by the App itself. It rather takes place in the " + - "IDP respectively the Relying Party. All communication is done with trustworhty backends, which is ensured" + - "by certificate pinning." -) -@Requirement( - "O.Arch_8", - "O.Plat_11", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Webviews only display local content that is delivered together with the application. " + - "Javascript is disabled, linking and loading other content is disabled. " + - "All website open the system browser." -) -@Requirement( - "O.Source_5", - "O.Source_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Secure deletion in memory is not possible with the technologies used, since automatic memory " + - "management is used. If there are errors in establishing the connection, the request is aborted. " + - "Access to sensitive data in the backend is only possible after successful authentication." -) -@Requirement( - "O.Source_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Automatic memory management is used in which the application cannot influence write and read access." -) -@Requirement( - "O.TrdP_1", - "O.TrdP_2", - "O.TrdP_3", - "O.TrdP_4", - "O.TrdP_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We use a dependency check that is evaluated cyclically. All libraries are source code dependencies" + - "The dependencies are checked for vulnerabilities as part of the SAST/SCA checks. " + - "We are currently in the evaluation phase for a new tool due to the discontinuation of " + - "Microfocus Fortify. We also use the Owasp dependency check in the build pipeline." -) -@Requirement( - "O.TrdP_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We do not share sensitive data with third parties. Se data usages within `Purp_8` and `O.Arch_2" -) -@Requirement( - "O.TrdP_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "See `O.Source_1` and `O.Arch_6." -) -@Requirement( - "O.Cryp_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Private keys for encryption are either created and stored within the key store or stored " + - "within the eGK. As encryption for Server communication is done ephemeral via ECDH-ES, no static " + - "private keys on client side are necessary." -) -@Requirement( - "O.Cryp_2", - "O.Cryp_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "All cryptographics are defined within `gemSpec_Krypt`. The document was created together with BSI." -) -@Requirement( - "O.Cryp_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We use Brainpool256R1 and AES256, see usages in O.Cryp_1 to O.Cryp_4" -) -@Requirement( - "O.Auth_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Our authentication concept is described in the following repository: https://github.com/gematik/" + - "api-erp/blob/master/docs/authentisieren.adoc We have more detailed diagrams regarding the context " + - "of the authentication in the SIS: Authentication on the central IDP: " + - "E-Rezept-App-Authentifizierungskonzept.pdf" + - "Authentication via Fast Track: E-Rezept-App-Authentifizierungskonzept_Fast_Track.pdf" -) -@Requirement( - "O.Auth_2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There is no client side separation of authentication and authorization. " + - "The authentication takes place at the identity provider, the authorization is realized by the Fachdienst" -) -@Requirement( - "O.Auth_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "One way to connect to the FD ist to login by using the eGK. To login with the eGK, the card, " + - "a CAN and a PIN is needed. The other way is to use a insurance company provided app. " + - "These apps must implement a second factor as well. " + - "See all `CardWall` prefixed files for all implementation details." -) -@Requirement( - "O.Auth_4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There is no step up from always using 2nd factor authentication. Tokens have short lifetimes " + - "of 12h (server defined) SSO-Tokens and 5min Access-Tokens." -) -@Requirement( - "O.Auth_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "A Audit Log for every FD access is available in each user profile." -) -@Requirement( - "O.Auth_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There are not passwords to guess for server login within our application. " + - "Only eGK login is directly available within the application. The users application password is not " + - "delayed, as any user with PIN access would have access to the unencrypted file system as well." -) -@Requirement( - "O.Auth_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Token invalidation happens after 12 hours. If a user is still active, a reauthentication via eGK, " + - "Biometrics or Insurance App is necessary. " + - "Each meaning the posession and or knowledge of the needed user input." -) -@Requirement( - "O.Auth_10", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Authentication via eGK cannot be altered, as the physical card cannot be modified without " + - "authentication (e.g. PIN change). See gemSpec_COS for details. Adding a authentication key that " + - "is secured via biometrics (labeld as \"save login\" within the cardwall) enforces a new " + - "authentication via eGK on server side." -) -@Requirement( - "O.Auth_12", - "O.Data_19", - "O.Plat_4", - "O.Plat_5", - "O.Plat_8", - "O.Plat_9", - "O.Resi_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "unused" -) -@Requirement( - "O.Plat_16", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Since we implement the \"security-by-default\" principle, we don't have additional security " + - "measures which the user can use to increase the security level. All measures are built-in." -) -@Requirement( - "O.Pass_4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There is no protocol for the application password within the application. " -) -@Requirement( - "O.Sess_1", "O.Sess_2", "O.Sess_3", "O.Sess_4", "O.Sess_5", "O.Sess_6", "O.Sess_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "not applicable" -) -@Requirement( - "O.Paid_1", "O.Paid_2", "O.Paid_3", "O.Paid_4", "O.Paid_5", "O.Paid_6", "O.Paid_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The app does not offer any purchases." -) -@Requirement( - "O.Tokn_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "For long-lived tokens, the token is stored in the secured realm database. Ephemeral tokens, " + - "such as those used in the biometric pairing process, do not persist and only remain in memory." -) -@Requirement( - "O.Tokn_2", - "O.Tokn_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The token is created by the backend, we have no means of manipulating the content." -) -@Requirement( - "O.Tokn_4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The token is created by the IDP and signed there. We have not valid signing identity " + - "within the application to sign the token." -) -@Requirement( - "O.Tokn_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "A Section within User Profiles contains all tokens for that profile on the device." -) -@Requirement( - "O.Data_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Secure deletion in memory is not possible with the technologies used, since automatic memory " + - "management is used. Ephemeral private keys are released as soon as they are no longer needed" -) -@Requirement( - "O.Purp_2", - "O.Data_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Collected data is sparse and use case related as required." -) -@Requirement( - "O.Purp_8", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The purpose of data usage is described in section 4 and 5 of the Data Terms. " + - "Interfaces to external services can be found in the following package: " + - "common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/*" -) -@Requirement( - "O.Purp_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The purposes are described in the data terms in sections 4 and 5. " + - "All data presented are necessary besides the display of the tokens by the IDP. " + - "However, these are presented in the app due to O.Token_5." -) -@Requirement( - "O.Data_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Private data is not initially created by the application. Only additional information, such as " + - "redeeming or deleting prescriptions create data. The created data is kept on the FD." -) -@Requirement( - "O.Data_12", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There is no API that allows extraction of biometric data." -) -@Requirement( - "O.Data_15", - sourceSpecification = "BSI-eRp-ePA", - rationale = "All user data is stored in the private data area of the app and thus, cannot be accessed by another " + - "app. Furthermore, we have only encrypted data stored on the device. This excludes the health data from the " + - "backend, which can be accessed with legitimate authentication from other devices as well." -) -@Requirement( - "O.Data_16", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Storing application data is not possible." -) -@Requirement( - "O.Data_17", - "O.Data_18", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Uninstalling the app will delete all data stored by the e-prescription app on the device. By " + - "default, files that created on internal storage are accessible only to the app. Android implements " + - "this protection by default. The user may choose to manually logout or delete profiles before app deletion." -) -@Requirement( - "O.Ntwk_4", - "O.Ntwk_7", - "O.Resi_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "See network_security_config and AndroidManifest.xml for settings and pinned domains," + - " no exceptions are made" -) -@Requirement( - "O.Ntwk_5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "By using OkHttp-Client handles that within the application." -) -@Requirement( - "O.Ntwk_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The server uses extended validation certificates to ensure maximum authenticity." -) -@Requirement( - "O.Ntwk_8", - "O.Ntwk_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "see results of backend system assessment." -) -@Requirement( - "O.Plat_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We test for device security at startup and show a dialog if the device is not secured." -) -@Requirement( - "O.Plat_2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "See AndroidManifest.xml for all accessed entitlements." -) -@Requirement( - "O.Plat_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We use the platform dialoges for this." -) -@Requirement( - "O.Plat_6", - "O.Plat_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Implemented by the OS Sandboxing." -) -@Requirement( - "O.Plat_10", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Interprocesscommunication is implemented using Universal Linking. Current use cases are limited " + - "to login with insurance company apps. No sensitive data is transferred, the actual payload is decided " + - "by the server." -) -@Requirement( - "O.Plat_12", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We use the platform dialoges for this." -) -@Requirement( - "O.Resi_1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The application guides the user through the onboarding process and suggests the user the most " + - "secure mechanisms available on this device." -) -@Requirement( - "O.Resi_8", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Does not make sense. This Project is an open source project." -) -@Requirement( - "O.Resi_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "We use API keys that are assigned to fixed app versions at the backend service. " + - "We also have minimum requirements for the supported platform versions. " + - "There are minimum supported API levels, currently >= 24" -) -@Requirement( - "O.Resi_10", - sourceSpecification = "BSI-eRp-ePA", - rationale = "There are persistent data in the app, which means that the app is temporarily resilient to the " + - "unavailability of the backend service. As soon as data is persisted on the device, it can be " + - "accessed again even after local disruptions." -) -@Suppress("LongMethod") -@Composable -fun MainScreenNavigation( - bottomSheetNavigator: BottomSheetNavigator, - navController: NavHostController -) { - val dependencyInjector = LocalDi.current - - /** - * Main screen navigation start check - */ - val mainScreenController = rememberMainScreenController() - - val onboardingSucceeded = mainScreenController.onboardingSucceeded - - val startDestinationScreen = shouldOnboardingBeDone(onboardingSucceeded) - - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - val mlKitAccepted by mainScreenController.mlKitAcceptedState - trackPopUps(analytics, analyticsState) - var previousNavEntry by remember { mutableStateOf("main") } - trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - val navigationMode by navController.navigationModeState(DeprecatedOnboardingNavigationScreens.Onboarding.route) - - ModalBottomSheetLayout(bottomSheetNavigator) { - NavHost( - navController, - startDestination = AppSecurityRoutes.subGraphName() - ) { - appSecurityGraph(navController = navController) { - navController.navigateAndClearStack(route = startDestinationScreen) - } - onboardingGraph( - navController, - startDestination = startDestinationScreen - ) - composable(MainNavigationScreens.Prescriptions.route) { - @Requirement( - "O.Plat_1#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for insecure Devices on MainScreen." - ) - MainScreenScaffoldContainer( - mainNavController = navController, - mainScreenController = mainScreenController, - onClickAddPrescription = { - if (mlKitAccepted) { - navController.navigate(PrescriptionRoutes.PrescriptionScanScreen.path()) - } else { - navController.navigate(MlKitRoutes.MlKitScreen.path()) - } - } - ) - } - mlKitGraph(navController = navController) - pkvGraph(navController = navController) - prescriptionGraph(navController = navController) - prescriptionDetailGraph(navController = navController) - profileGraph(navController = navController) - composable( - MainNavigationScreens.Pharmacies.route, - MainNavigationScreens.Pharmacies.arguments - ) { - PharmacyNavigation( - onBack = { - navController.popBackStack() - }, - onFinish = { - navController.navigate(MainNavigationScreens.Prescriptions.route) - } - ) - } - composable( - MainNavigationScreens.Redeem.route - ) { - RedeemNavigation( - onFinish = { - navController.navigate(MainNavigationScreens.Prescriptions.route) - } - ) - } - composable( - MainNavigationScreens.Messages.route, - MainNavigationScreens.Messages.arguments - ) { - val orderId = - remember { it.arguments?.getString("orderId")!! } - OrderOverviewScreen( - orderId = orderId, - mainNavController = navController - ) - } - composable(MainNavigationScreens.Debug.route) { - DebugScreenWrapper(navController) - } - composable(MainNavigationScreens.OrderHealthCard.route) { - HealthCardContactOrderScreen(onBack = { navController.popBackStack() }) - } - composable( - MainNavigationScreens.Archive.route - ) { - val prescriptionController = rememberPrescriptionsController() - - NavigationAnimation(mode = navigationMode) { - ArchiveScreen( - prescriptionsController = prescriptionController, - navController = navController - ) { - navController.popBackStack() - } - } - } - exampleScreensGraph(navController = navController) - trackingGraph(navController) - settingsGraph(navController = navController) - troubleShootingGraph(navController = navController) - cardUnlockGraph( - dependencyInjector = dependencyInjector, - navController = navController - ) - cardWallGraph( - dependencyInjector = dependencyInjector, - navController = navController - ) - pharmacyGraph( - dependencyInjector = dependencyInjector, - navController = navController - ) - } - } -} - -@Composable -private fun shouldOnboardingBeDone(onboardingSucceeded: Boolean): String = - when (onboardingSucceeded) { - true -> MainNavigationScreens.Prescriptions.route - false -> OnboardingRoutes.OnboardingWelcomeScreen.route - } - -@Composable -fun calculatePrescriptionCount(mainScreenController: MainScreenController): Int { - var prescriptionCount = 0 - var refreshEvent by remember { mutableStateOf(null) } - - LaunchedEffect(Unit) { - mainScreenController.onRefreshEvent.collect { - refreshEvent = it - } - } - refreshEvent?.let { - when (it) { - is RefreshedState -> { - prescriptionCount = it.nrOfNewPrescriptions - } - } - } - return prescriptionCount -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt index e7948afb..6e72cbd0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt @@ -1,28 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.navigation import android.os.Parcelable -import androidx.navigation.NavType -import androidx.navigation.navArgument import de.gematik.ti.erp.app.analytics.PopUpName +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutes import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -32,32 +33,13 @@ import kotlinx.serialization.Serializable data class TaskIds(val ids: List) : Parcelable, List by ids object MainNavigationScreens { - object Onboarding : Routes("onboarding") - object Prescriptions : Routes("main") - object Archive : Routes("main_prescriptionArchive") - - object Orders : Routes("orders") - - object Messages : Routes( - "orders_detail", - navArgument("orderId") { type = NavType.StringType } - ) { - fun path(orderId: String) = - Messages.path("orderId" to orderId) - } - - object Pharmacies : Routes("pharmacySearch") - - object Redeem : Routes("redeem_methodSelection") - object Debug : Routes("debug") - object OrderHealthCard : Routes("contactInsuranceCompany") } val MainScreenBottomNavigationItems = listOf( - MainNavigationScreens.Prescriptions, - MainNavigationScreens.Pharmacies, - MainNavigationScreens.Orders, + PrescriptionRoutes.PrescriptionsScreen, + PharmacyRoutes.PharmacyStartScreen, + MessagesRoutes.MessageListScreen, SettingsNavigationScreens.SettingsScreen ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/NavigationGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/NavigationGraph.kt new file mode 100644 index 00000000..7b7d9fbf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/NavigationGraph.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.navigation + +import android.annotation.SuppressLint +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.compose.composable +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import de.gematik.ti.erp.app.analytics.navigation.trackingGraph +import de.gematik.ti.erp.app.app.ApplicationInnerPadding +import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes +import de.gematik.ti.erp.app.appsecurity.navigation.appSecurityGraph +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.cardunlock.navigation.cardUnlockGraph +import de.gematik.ti.erp.app.cardwall.navigation.cardWallGraph +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.LocalBottomSheetNavigator +import de.gematik.ti.erp.app.core.LocalDi +import de.gematik.ti.erp.app.core.LocalNavController +import de.gematik.ti.erp.app.debugsettings.navigation.showcaseScreensGraph +import de.gematik.ti.erp.app.mainscreen.presentation.rememberAppController +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes +import de.gematik.ti.erp.app.messages.navigation.messagesGraph +import de.gematik.ti.erp.app.mlkit.navigation.mlKitGraph +import de.gematik.ti.erp.app.navigation.NavigationGraphBuilder +import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes +import de.gematik.ti.erp.app.onboarding.navigation.onboardingGraph +import de.gematik.ti.erp.app.orderhealthcard.navigation.orderHealthCardGraph +import de.gematik.ti.erp.app.pharmacy.navigation.pharmacyGraph +import de.gematik.ti.erp.app.pkv.navigation.pkvGraph +import de.gematik.ti.erp.app.prescription.detail.navigation.prescriptionDetailGraph +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.prescription.navigation.prescriptionGraph +import de.gematik.ti.erp.app.medicationplan.navigation.medicationPlanGraph +import de.gematik.ti.erp.app.profiles.navigation.profileGraph +import de.gematik.ti.erp.app.redeem.navigation.redeemGraph +import de.gematik.ti.erp.app.settings.navigation.settingsGraph +import de.gematik.ti.erp.app.troubleshooting.navigation.troubleShootingGraph +import de.gematik.ti.erp.app.ui.DebugScreenWrapper +import de.gematik.ti.erp.app.userauthentication.navigation.UserAuthenticationRoutes +import de.gematik.ti.erp.app.userauthentication.navigation.userAuthenticationGraph +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod + +@SuppressLint("RestrictedApi") +@OptIn(ExperimentalMaterialNavigationApi::class) +@Suppress("LongMethod") +@Composable +fun NavigationGraph( + authentication: AuthenticationModeAndMethod?, + isDemoMode: Boolean, + padding: ApplicationInnerPadding +) { + val dependencyInjector = LocalDi.current + val bottomSheetNavigator = LocalBottomSheetNavigator.current + val navHostController = LocalNavController.current + + LocalActivity.current.setApplicationInnerPadding(padding) + val currentActivity = LocalActivity.current as BaseActivity + + val mainScreenController = rememberAppController() + + val onboardingSucceeded = mainScreenController.onboardingSucceeded + val showMedicationSuccess by currentActivity.shouldShowMedicationSuccess.collectAsStateWithLifecycle() + + val startDestinationScreen = shouldOnboardingBeDone(onboardingSucceeded, showMedicationSuccess) + + val currentBackStack by navHostController.currentBackStack.collectAsStateWithLifecycle() + + DisposableEffect(showMedicationSuccess) { + if (showMedicationSuccess && currentBackStack.isNotEmpty()) { + navHostController.navigate(MedicationPlanRoutes.MedicationPlanNotificationSuccess.path()) + } + onDispose { + currentActivity.medicationSuccessHasBeenShown() + } + } + LaunchedEffect(authentication) { + if (authentication is AuthenticationModeAndMethod.AuthenticationRequired && !isDemoMode) { + navHostController.navigate(UserAuthenticationRoutes.UserAuthenticationScreen.path()) + } + } + + NavigationGraphBuilder( + bottomSheetNavigator = bottomSheetNavigator, + navHostController = navHostController, + startDestination = AppSecurityRoutes.subGraphName() + ) { + appSecurityGraph(navController = navHostController) { + navHostController.navigateAndClearStack(route = startDestinationScreen) + } + onboardingGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + mlKitGraph(navController = navHostController) + pkvGraph(navController = navHostController) + prescriptionGraph(navController = navHostController) + prescriptionDetailGraph(navController = navHostController) + messagesGraph(navController = navHostController) + profileGraph(navController = navHostController) + pharmacyGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + orderHealthCardGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + showcaseScreensGraph(navController = navHostController) + trackingGraph(navController = navHostController) + settingsGraph(navController = navHostController) + troubleShootingGraph(navController = navHostController) + cardUnlockGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + medicationPlanGraph(navController = navHostController) + cardWallGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + redeemGraph( + dependencyInjector = dependencyInjector, + navController = navHostController + ) + userAuthenticationGraph(navController = navHostController) + composable(MainNavigationScreens.Debug.route) { + DebugScreenWrapper(navHostController) + } + } +} + +private fun shouldOnboardingBeDone(onboardingSucceeded: Boolean, showMedicationSuccess: Boolean): String = + when (onboardingSucceeded) { + true -> { + if (showMedicationSuccess) { + MedicationPlanRoutes.MedicationPlanNotificationSuccess.path() + } else { + PrescriptionRoutes.PrescriptionsScreen.path() + } + } + false -> OnboardingRoutes.OnboardingWelcomeScreen.path() + } + +private fun ComponentActivity.setApplicationInnerPadding( + padding: ApplicationInnerPadding +) { + (this as? BaseActivity)?.applicationInnerPadding = padding +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/AppController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/AppController.kt new file mode 100644 index 00000000..5e8936ae --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/AppController.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.NetworkStatusTracker +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.messages.domain.usecase.GetUnreadMessagesCountUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetDownloadResourcesDetailStateUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase +import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Suppress("LongParameterList", "StaticFieldLeak", "ConstructorParameterNaming") +class AppController( + networkStatusTracker: NetworkStatusTracker, + getActiveProfileUseCase: GetActiveProfileUseCase, + getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, + getOnboardingSucceededUseCase: GetOnboardingSucceededUseCase, + downloadResourcesStateUseCase: GetDownloadResourcesDetailStateUseCase, + private val getUnreadMessagesCountUseCase: GetUnreadMessagesCountUseCase, + private val saveWelcomeDrawerShownUseCase: SaveWelcomeDrawerShownUseCase, + private val _unreadOrders: MutableStateFlow = MutableStateFlow(0L), + private val _orderedEvent: MutableStateFlow = MutableStateFlow(null) +) : GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = { profile, coroutineScope -> + coroutineScope.launch { + _unreadOrders.value = getUnreadMessagesCountUseCase.invoke(profile.id).first() + } + } +) { + + enum class OrderedEvent { + Success, + Error + } + + val orderedEvent: StateFlow = _orderedEvent + val unreadOrders: StateFlow = _unreadOrders + + val isNetworkConnected: StateFlow = networkStatusTracker.networkStatus + .stateIn( + scope = controllerScope, + started = SharingStarted.WhileSubscribed(), + initialValue = true + ) + + fun resetOrderedEvent() { + _orderedEvent.value = null + } + + private val isScreenshotsAllowed = getScreenShotsAllowedUseCase.invoke() + + @Requirement( + "O.Resi_1#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Screenshots are disabled by default" + ) + val screenshotsState + @Composable + get() = isScreenshotsAllowed.collectAsStateWithLifecycle(false) + + val refreshState = downloadResourcesStateUseCase.invoke() + + val onboardingSucceeded = getOnboardingSucceededUseCase.invoke() + + fun welcomeDrawerShown() = controllerScope.launch { + saveWelcomeDrawerShownUseCase() + } + + suspend fun updateUnreadOrders(profile: ProfilesUseCaseData.Profile) { + _unreadOrders.value = getUnreadMessagesCountUseCase.invoke(profile.id).first() + } + + fun onOrdered(hasError: Boolean) { + _orderedEvent.value = if (hasError) OrderedEvent.Error else OrderedEvent.Success + } +} + +@Composable +fun rememberAppController(): AppController { + val getActiveProfileUseCase by rememberInstance() + val appController by rememberInstance() + + val activeProfile by getActiveProfileUseCase().collectAsStateWithLifecycle(null) + + // to have a singleton instance of AppController + return rememberSaveable(activeProfile, saver = complexAutoSaver()) { appController } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/ExternalAuthenticationHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/ExternalAuthenticationHandler.kt index ab5a2b37..9d342e1d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/ExternalAuthenticationHandler.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/ExternalAuthenticationHandler.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.presentation import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.core.GidResultIntent import de.gematik.ti.erp.app.idp.model.error.DecryptAccessTokenError import de.gematik.ti.erp.app.idp.model.error.SingleSignOnTokenError import de.gematik.ti.erp.app.idp.model.error.UniversalLinkError @@ -54,18 +54,14 @@ class ExternalAuthenticationHandler( /** * Handles an incoming intent. Updates the [handlerState] to Success if it can be handled. */ - @Requirement( - "O.Plat_10#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "handle incoming intent" - ) - suspend fun handle(value: String) = + suspend fun handle(intent: GidResultIntent) = try { handlerState.value = AuthenticationHandlerState.Loading - Napier.d("Authenticate external ...") - idpUseCase.invoke(URI(value)) + Napier.i("Authenticate with GID ...") + idpUseCase.invoke(URI(intent.uriData)) handlerState.value = AuthenticationHandlerState.Success - Napier.d("... authenticated") + intent.resultChannel.send(intent.uriData) // the result is sent here, after the idpUseCase saves the token + Napier.i("... authenticated") } catch (e: Throwable) { when (e) { is DecryptAccessTokenError -> handlerState.value = AuthenticationHandlerState.AuthTokenNotSaved diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt deleted file mode 100644 index 4d00ecbb..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.consent.usecase.SaveGrantConsentDrawerShownUseCase -import de.gematik.ti.erp.app.consent.usecase.ShowGrantConsentUseCase -import de.gematik.ti.erp.app.orders.usecase.GetUnreadOrdersUseCase -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.usecase.GetCanStartToolTipsUseCase -import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase -import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase -import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase -import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveToolTippsShownUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -@Suppress("LongParameterList") -class MainScreenController( - private val getUnreadOrdersUseCase: GetUnreadOrdersUseCase, - private val saveToolTipsShownUseCase: SaveToolTippsShownUseCase, - private val saveWelcomeDrawerShownUseCase: SaveWelcomeDrawerShownUseCase, - private val saveGrantConsentDrawerShownUseCase: SaveGrantConsentDrawerShownUseCase, - private val getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, - private val getOnboardingSucceededUseCase: GetOnboardingSucceededUseCase, - private val getCanStartToolTipsUseCase: GetCanStartToolTipsUseCase, - private val getShowWelcomeDrawerUseCase: GetShowWelcomeDrawerUseCase, - private val showGrantConsentUseCase: ShowGrantConsentUseCase, - private val getMLKitAcceptedUseCase: GetMLKitAcceptedUseCase, - private val scope: CoroutineScope -) { - - enum class OrderedEvent { - Success, - Error - } - - private val _onRefreshEvent = MutableSharedFlow() - val onRefreshEvent: Flow - get() = _onRefreshEvent - - var orderedEvent: OrderedEvent? by mutableStateOf(null) - private set - - fun resetOrderedEvent() { - orderedEvent = null - } - - private val screenshotsAllowed = - getScreenShotsAllowedUseCase.invoke() - - val screenshotsState - @Composable - get() = screenshotsAllowed.collectAsStateWithLifecycle(false) - - val onboardingSucceeded = getOnboardingSucceededUseCase.invoke() - - private val canStartToolTips = getCanStartToolTipsUseCase.invoke() - - val canStartToolTipsState - @Composable - get() = canStartToolTips.collectAsStateWithLifecycle(false) - - fun toolTipsShown() = scope.launch { - saveToolTipsShownUseCase() - } - - private val showWelcomeDrawer = - getShowWelcomeDrawerUseCase.invoke() - - val showWelcomeDrawerState - @Composable - get() = showWelcomeDrawer.collectAsStateWithLifecycle(false) - - fun welcomeDrawerShown() = scope.launch { - saveWelcomeDrawerShownUseCase() - } - - private val showGiveConsentDrawer = - showGrantConsentUseCase.invoke() - - val showGiveConsentDrawerState - @Composable - get() = showGiveConsentDrawer.collectAsStateWithLifecycle(false) - - fun giveConsentDrawerShown(profileId: ProfileIdentifier) = scope.launch { - saveGrantConsentDrawerShownUseCase(profileId) - } - - fun updateUnreadOrders(profile: ProfilesUseCaseData.Profile) = getUnreadOrdersUseCase.invoke(profile.id) - - suspend fun onRefresh(event: PrescriptionServiceState) { - _onRefreshEvent.emit(event) - } - - fun onOrdered(hasError: Boolean) { - orderedEvent = if (hasError) OrderedEvent.Error else OrderedEvent.Success - } - - private val mlKitAccepted = - getMLKitAcceptedUseCase.invoke() - - val mlKitAcceptedState - @Composable - get() = mlKitAccepted.collectAsStateWithLifecycle(false) -} - -@Composable -fun rememberMainScreenController(): MainScreenController { - val getUnreadOrdersUseCase by rememberInstance() - val getScreenShotsAllowedUseCase by rememberInstance() - val shouldShowOnboardingUseCase by rememberInstance() - val getMLKitAcceptedUseCase by rememberInstance() - val getShowToolTipsUseCase by rememberInstance() - val saveToolTipsShownUseCase by rememberInstance() - val getShowWelcomeDrawerUseCase by rememberInstance() - val saveWelcomeDrawerShownUseCase by rememberInstance() - val getShowGiveConsentDrawerUseCase by rememberInstance() - val saveGrantConsentDrawerShownUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - return remember { - MainScreenController( - getUnreadOrdersUseCase = getUnreadOrdersUseCase, - getScreenShotsAllowedUseCase = getScreenShotsAllowedUseCase, - getOnboardingSucceededUseCase = shouldShowOnboardingUseCase, - getMLKitAcceptedUseCase = getMLKitAcceptedUseCase, - getCanStartToolTipsUseCase = getShowToolTipsUseCase, - saveToolTipsShownUseCase = saveToolTipsShownUseCase, - getShowWelcomeDrawerUseCase = getShowWelcomeDrawerUseCase, - showGrantConsentUseCase = getShowGiveConsentDrawerUseCase, - saveGrantConsentDrawerShownUseCase = saveGrantConsentDrawerShownUseCase, - saveWelcomeDrawerShownUseCase = saveWelcomeDrawerShownUseCase, - scope = scope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ExternalAuthenticationHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ExternalAuthenticationHandler.kt index 954d21e2..cacad6c3 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ExternalAuthenticationHandler.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ExternalAuthenticationHandler.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -32,13 +32,14 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.usecase.AuthenticateWithExternalHealthInsuranceAppUseCase import de.gematik.ti.erp.app.mainscreen.presentation.AuthenticationHandlerState import de.gematik.ti.erp.app.mainscreen.presentation.ExternalAuthenticationHandler +import de.gematik.ti.erp.app.utils.compose.LoadingDialog import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar import de.gematik.ti.erp.app.utils.extensions.SnackbarScaffold import org.kodein.di.compose.rememberInstance @Composable -fun ExternalAuthenticationDialog() { +fun ExternalAuthenticationUiHandler() { val intentHandler = LocalIntentHandler.current val snackbar = LocalSnackbar.current @@ -53,7 +54,7 @@ fun ExternalAuthenticationDialog() { showUniversalUrl( snackbar = snackbar, context = context, - url = it + url = it.uriData ) authenticationHandler.handle(it) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/LoadingDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/LoadingDialog.kt deleted file mode 100644 index 7c3342e2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/LoadingDialog.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.foundation.layout.size -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LoadingDialog( - onDismissRequest: () -> Unit -) { - AlertDialog( - modifier = Modifier.size(SizeDefaults.fivefold), - onDismissRequest = onDismissRequest - ) { - CircularProgressIndicator( - modifier = Modifier.size(SizeDefaults.doubleHalf), - color = AppTheme.colors.primary400 - ) - } -} - -@LightDarkPreview -@Composable -fun LoadingDialogPreview() { - PreviewAppTheme { - LoadingDialog { - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt index 9f26c9ba..503d17ee 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -28,9 +28,9 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChatBubbleOutline import androidx.compose.material.icons.outlined.PinDrop import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.ShoppingBag import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -41,18 +41,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.compose.currentBackStackEntryAsState import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenBottomNavigationItems -import de.gematik.ti.erp.app.mainscreen.navigation.calculatePrescriptionCount -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.profiles.presentation.ProfileController +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutes +import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults import de.gematik.ti.erp.app.utils.compose.BottomNavigation private const val BottomBarBadgeOffsetX = -5 @@ -61,20 +61,12 @@ private const val BottomBarBadgeOffsetY = 5 @Suppress("LongMethod", "ComplexMethod") @Composable internal fun MainScreenBottomBar( - navController: NavController, - bottomNavController: NavController, - profileController: ProfileController, - mainScreenController: MainScreenController + mainNavController: NavController, + unreadOrdersCount: Long ) { - val navBackStackEntry by bottomNavController.currentBackStackEntryAsState() + val navBackStackEntry by mainNavController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - val activeProfile by profileController.getActiveProfileState() - - val unreadPrescriptionCount = calculatePrescriptionCount(mainScreenController) - val unreadOrdersCount by mainScreenController.updateUnreadOrders(activeProfile) - .collectAsStateWithLifecycle(0) - BottomNavigation( backgroundColor = MaterialTheme.colors.surface, extraContent = {} @@ -83,9 +75,9 @@ internal fun MainScreenBottomBar( BottomNavigationItem( modifier = Modifier.testTag( when (screen) { - MainNavigationScreens.Prescriptions -> TestTag.BottomNavigation.PrescriptionButton - MainNavigationScreens.Orders -> TestTag.BottomNavigation.OrdersButton - MainNavigationScreens.Pharmacies -> TestTag.BottomNavigation.PharmaciesButton + PrescriptionRoutes.PrescriptionsScreen -> TestTag.BottomNavigation.PrescriptionButton + MessagesRoutes.MessageListScreen -> TestTag.BottomNavigation.OrdersButton + PharmacyRoutes.PharmacyStartScreen -> TestTag.BottomNavigation.PharmaciesButton SettingsNavigationScreens.SettingsScreen -> TestTag.BottomNavigation.SettingsButton else -> "" } @@ -95,41 +87,20 @@ internal fun MainScreenBottomBar( icon = { Column(horizontalAlignment = Alignment.CenterHorizontally) { when (screen) { - MainNavigationScreens.Prescriptions -> - if (unreadPrescriptionCount > 0) { - BadgedBox( - badge = { - Badge( - modifier = Modifier.offset( - x = BottomBarBadgeOffsetX.dp, - y = BottomBarBadgeOffsetY.dp - ), - backgroundColor = Color.Red, - contentColor = Color.White - ) { Text(unreadPrescriptionCount.toString()) } - } - ) { - Icon( - painterResource(R.drawable.ic_logo_outlined), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - } else { - Icon( - painterResource(R.drawable.ic_logo_outlined), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } + PrescriptionRoutes.PrescriptionsScreen -> + Icon( + painterResource(R.drawable.ic_logo_outlined), + contentDescription = null, + modifier = Modifier.size(SizeDefaults.triple) + ) - MainNavigationScreens.Pharmacies -> Icon( + PharmacyRoutes.PharmacyStartScreen -> Icon( Icons.Outlined.PinDrop, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(SizeDefaults.triple) ) - MainNavigationScreens.Orders -> + MessagesRoutes.MessageListScreen -> if (unreadOrdersCount > 0) { BadgedBox( badge = { @@ -144,16 +115,16 @@ internal fun MainScreenBottomBar( } ) { Icon( - Icons.Outlined.ShoppingBag, + Icons.Outlined.ChatBubbleOutline, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(SizeDefaults.triple) ) } } else { Icon( - Icons.Outlined.ShoppingBag, + Icons.Outlined.ChatBubbleOutline, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(SizeDefaults.triple) ) } @@ -169,9 +140,9 @@ internal fun MainScreenBottomBar( Text( stringResource( when (screen) { - MainNavigationScreens.Prescriptions -> R.string.pres_bottombar_prescriptions - MainNavigationScreens.Orders -> R.string.pres_bottombar_orders - MainNavigationScreens.Pharmacies -> R.string.pres_bottombar_pharmacies + PrescriptionRoutes.PrescriptionsScreen -> R.string.pres_bottombar_prescriptions + MessagesRoutes.MessageListScreen -> R.string.messages_bottombar + PharmacyRoutes.PharmacyStartScreen -> R.string.pres_bottombar_pharmacies SettingsNavigationScreens.SettingsScreen -> R.string.main_settings_acc else -> R.string.pres_bottombar_prescriptions } @@ -183,13 +154,20 @@ internal fun MainScreenBottomBar( selected = currentRoute == screen.route, alwaysShowLabel = true, onClick = { - if (currentRoute != screen.route) { - when (screen.route) { - MainNavigationScreens.Pharmacies.route -> - navController.navigate(screen.path()) - - else -> - bottomNavController.navigate(screen.path()) + when (screen) { + is PharmacyRoutes.PharmacyStartScreen -> { + mainNavController.navigate(screen.path("")) + } + is SettingsNavigationScreens.SettingsScreen -> { + mainNavController.navigate(screen.path()) + } + is MessagesRoutes.MessageListScreen -> { + mainNavController.navigate(screen.path()) + } + is PrescriptionRoutes.PrescriptionsScreen -> { + mainNavController.navigate(screen.path()) + // on reaching here the back navigation only closes the app + mainNavController.navigateAndClearStack(route = PrescriptionRoutes.PrescriptionsScreen.route) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt index d3ecafd5..0c753ee0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -30,13 +30,11 @@ import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -46,38 +44,37 @@ import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.model.EditProfilePictureSelectionState import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenBottomPopUpNames -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.AppController +import de.gematik.ti.erp.app.mainscreen.ui.components.EditProfilePicture import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.ProfileController -import de.gematik.ti.erp.app.profiles.ui.AvatarPicker -import de.gematik.ti.erp.app.profiles.ui.ColorPicker -import de.gematik.ti.erp.app.profiles.ui.ProfileImage +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import java.util.UUID @Stable sealed class MainScreenBottomSheetContentState { @Stable class EditProfilePicture( - val popUp: MainScreenBottomPopUpNames.EditProfilePicture = MainScreenBottomPopUpNames.EditProfilePicture + val popUp: MainScreenBottomPopUpNames.EditProfilePicture = MainScreenBottomPopUpNames.EditProfilePicture, + val profile: ProfilesUseCaseData.Profile ) : MainScreenBottomSheetContentState() @Stable class EditProfileName( - val popUp: MainScreenBottomPopUpNames.EditProfileName = MainScreenBottomPopUpNames.EditProfileName + val popUp: MainScreenBottomPopUpNames.EditProfileName = MainScreenBottomPopUpNames.EditProfileName, + val profile: ProfilesUseCaseData.Profile ) : MainScreenBottomSheetContentState() @Stable class AddProfile( - val popUp: MainScreenBottomPopUpNames.AddProfile = MainScreenBottomPopUpNames.AddProfile ) : MainScreenBottomSheetContentState() @@ -92,18 +89,24 @@ sealed class MainScreenBottomSheetContentState { ) : MainScreenBottomSheetContentState() } +@OptIn(ExperimentalComposeUiApi::class) @Composable fun MainScreenBottomSheetContentState( mainNavController: NavController, - mainScreenController: MainScreenController, - profileController: ProfileController, + appController: AppController, + profile: ProfilesUseCaseData.Profile, + existingProfiles: List, infoContentState: MainScreenBottomSheetContentState?, - profileToRename: ProfilesUseCaseData.Profile, + keyboardController: SoftwareKeyboardController?, onGrantConsent: () -> Unit, + onUpdateProfileName: (ProfileIdentifier, String) -> Unit, + onAddProfileName: (String) -> Unit, + onClearPersonalizedImage: (ProfileIdentifier) -> Unit, + onPickImage: (EditProfilePictureSelectionState) -> Unit, + onSelectAvatar: (ProfileIdentifier, ProfilesData.Avatar) -> Unit, + onSelectProfileColor: (ProfilesUseCaseData.Profile, ProfilesData.ProfileColorNames) -> Unit, onCancel: () -> Unit ) { - val profile by profileController.getActiveProfileState() - val title = when (infoContentState) { is MainScreenBottomSheetContentState.EditProfilePicture -> stringResource(R.string.mainscreen_bottom_sheet_edit_profile_image) @@ -127,7 +130,11 @@ fun MainScreenBottomSheetContentState( ) { title?.let { SpacerMedium() - Text(it, style = AppTheme.typography.subtitle1) + Text( + text = it, + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral999 + ) SpacerMedium() } LazyColumn( @@ -137,66 +144,63 @@ fun MainScreenBottomSheetContentState( infoContentState?.let { when (it) { is MainScreenBottomSheetContentState.EditProfilePicture -> - EditProfileAvatar( - profile = profile, + EditProfilePicture( + profile = it.profile, clearPersonalizedImage = { - profileController.clearPersonalizedImage(profile.id) - }, - onPickPersonalizedImage = { - mainNavController.navigate( - ProfileRoutes.ProfileImageCropperScreen.path( - profileId = profile.id - ) - ) + onClearPersonalizedImage(it.profile.id) }, + onPickImage = onPickImage, onSelectAvatar = { avatar -> - - profileController.saveAvatarFigure(profile.id, avatar) + onSelectAvatar(it.profile.id, avatar) }, onSelectProfileColor = { color -> - - profileController.updateProfileColor(profile, color) + onSelectProfileColor(it.profile, color) } ) - is MainScreenBottomSheetContentState.EditProfileName -> + is MainScreenBottomSheetContentState.EditProfileName -> { ProfileSheetContent( - profileController = profileController, + key = UUID.randomUUID().toString(), + existingProfiles = existingProfiles, + keyboardController = keyboardController, addProfile = false, - profileToEdit = profileToRename, + profileToEdit = it.profile, + onUpdate = onUpdateProfileName, + onAdd = onAddProfileName, onCancel = onCancel ) + } is MainScreenBottomSheetContentState.AddProfile -> ProfileSheetContent( - profileController = profileController, + key = UUID.randomUUID().toString(), + existingProfiles = existingProfiles, + keyboardController = keyboardController, addProfile = true, profileToEdit = null, + onUpdate = onUpdateProfileName, + onAdd = onAddProfileName, onCancel = onCancel ) is MainScreenBottomSheetContentState.Welcome -> ConnectBottomSheetContent( onClickConnect = { - mainScreenController.welcomeDrawerShown() + appController.welcomeDrawerShown() mainNavController.navigate( CardWallRoutes.CardWallIntroScreen.path(profile.id) ) }, onCancel = { - mainScreenController.welcomeDrawerShown() + appController.welcomeDrawerShown() onCancel() } ) is MainScreenBottomSheetContentState.GrantConsent -> GrantConsentBottomSheetContent( - onClickGrantConsent = { - onGrantConsent() - }, - onCancel = { - onCancel() - } + onClickGrantConsent = onGrantConsent, + onCancel = onCancel ) } } @@ -205,74 +209,6 @@ fun MainScreenBottomSheetContentState( } } -@Composable -private fun EditProfileAvatar( - profile: ProfilesUseCaseData.Profile, - clearPersonalizedImage: () -> Unit, - onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.Avatar) -> Unit, - onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit -) { - ProfileColorAndImagePickerContent( - profile = profile, - clearPersonalizedImage = clearPersonalizedImage, - onPickPersonalizedImage = onPickPersonalizedImage, - onSelectAvatar = onSelectAvatar, - onSelectProfileColor = onSelectProfileColor - ) -} - -@Composable -private fun ProfileColorAndImagePickerContent( - profile: ProfilesUseCaseData.Profile, - clearPersonalizedImage: () -> Unit, - onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.Avatar) -> Unit, - onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit -) { - var editableProfile by remember { mutableStateOf(profile) } - Column(modifier = Modifier.fillMaxSize()) { - SpacerMedium() - ProfileImage(editableProfile) { - editableProfile = editableProfile.copy( - avatar = ProfilesData.Avatar.PersonalizedImage, - image = null - ) - clearPersonalizedImage() - } - - SpacerXXLarge() - AvatarPicker( - profile = editableProfile, - currentAvatar = editableProfile.avatar, - onPickPersonalizedImage = onPickPersonalizedImage, - onSelectAvatar = { - editableProfile = editableProfile.copy(avatar = it) - onSelectAvatar(it) - } - ) - - if (editableProfile.avatar != ProfilesData.Avatar.PersonalizedImage) { - SpacerXXLarge() - SpacerMedium() - Text( - stringResource(R.string.edit_profile_background_color), - style = AppTheme.typography.h6 - ) - SpacerLarge() - - ColorPicker( - profileColorName = editableProfile.color, - onSelectProfileColor = { - editableProfile = editableProfile.copy(color = it) - onSelectProfileColor(it) - } - ) - SpacerLarge() - } - } -} - @Composable private fun ConnectBottomSheetContent(onClickConnect: () -> Unit, onCancel: () -> Unit) { ConnectBottomSheet( @@ -326,7 +262,7 @@ private fun ConnectBottomSheet( ) SpacerSmall() Text( - info, + text = info, style = AppTheme.typography.body2l, textAlign = TextAlign.Center ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt deleted file mode 100644 index bd1d92a0..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.platform.testTag -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenContentNavHost -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.profiles.presentation.ProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens - -@Composable -internal fun MainScreenScaffold( - modifier: Modifier = Modifier, - mainScreenController: MainScreenController, - profileController: ProfileController, - mainNavController: NavController, - bottomNavController: NavHostController, - showToolTipps: Boolean, - tooltipBounds: MutableState>, - onClickAddProfile: () -> Unit, - onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, - onClickAvatar: () -> Unit, - onClickAddPrescription: () -> Unit, - scaffoldState: ScaffoldState -) { - val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) - val activeProfile by profileController.getActiveProfileState() - - val isInPrescriptionScreen by remember { - derivedStateOf { - currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route - } - } - var topBarElevated by remember { mutableStateOf(true) } - - Scaffold( - modifier = Modifier.testTag(TestTag.Main.MainScreen).then(modifier), - topBar = { - if (currentBottomNavigationRoute?.destination?.route != SettingsNavigationScreens.SettingsScreen.route) { - MultiProfileTopAppBar( - mainScreenController = mainScreenController, - profileController = profileController, - isInPrescriptionScreen = isInPrescriptionScreen, - showToolTipps = showToolTipps, - tooltipBounds = tooltipBounds, - elevated = topBarElevated, - onClickAddProfile = onClickAddProfile, - onClickChangeProfileName = onClickChangeProfileName, - onClickAddPrescription = onClickAddPrescription - ) - } - }, - bottomBar = { - MainScreenBottomBar( - navController = mainNavController, - mainScreenController = mainScreenController, - profileController = profileController, - bottomNavController = bottomNavController - ) - }, - floatingActionButton = { - if (isInPrescriptionScreen) { - RedeemFloatingActionButton( - activeProfile = activeProfile, - onClick = { - mainNavController.navigate(MainNavigationScreens.Redeem.path()) - } - ) - } - }, - scaffoldState = scaffoldState - ) { innerPadding -> - - MainScreenContentNavHost( - modifier = Modifier.padding(innerPadding), - mainScreenController = mainScreenController, - mainNavController = mainNavController, - bottomNavController = bottomNavController, - onClickAvatar = onClickAvatar, - onElevateTopBar = { - topBarElevated = it - }, - onClickArchive = { mainNavController.navigate(MainNavigationScreens.Archive.path()) } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt deleted file mode 100644 index 78ec1a9e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import android.accessibilityservice.AccessibilityServiceInfo -import android.content.Context -import android.net.Uri -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackMainScreenBottomPopUps -import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberConsentController -import de.gematik.ti.erp.app.pkv.ui.GrantConsentDialog -import de.gematik.ti.erp.app.pkv.ui.HandleConsentErrorState -import de.gematik.ti.erp.app.pkv.ui.HandleConsentState -import de.gematik.ti.erp.app.profiles.presentation.ProfileController.Companion.DEFAULT_EMPTY_PROFILE -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.utils.extensions.LocalDialog -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Suppress("LongMethod") -@Composable -internal fun MainScreenScaffoldContainer( - mainNavController: NavController, - mainScreenController: MainScreenController, - onClickAddPrescription: () -> Unit -) { - val context = LocalContext.current - - val showToolTips by mainScreenController.canStartToolTipsState - var startToolTips by remember { mutableStateOf(false) } - var startGetConsentBottomSheet by remember { mutableStateOf(false) } - val dialog = LocalDialog.current - val snackbar = LocalSnackbar.current - - val profileController = rememberProfileController() - val activeProfile by profileController.getActiveProfileState() - - val consentController = rememberConsentController(profile = activeProfile) - - val consentState by consentController.consentState.collectAsStateWithLifecycle() - val consentErrorState by consentController.consentErrorState.collectAsStateWithLifecycle() - - val bottomNavController = rememberNavController() - val scaffoldState = rememberScaffoldState() - - val showWelcomeDrawer by mainScreenController.showWelcomeDrawerState - val showGetConsentDrawer by mainScreenController.showGiveConsentDrawerState - - var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } - - val sheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true - ) - - val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) - - var previousNavEntry by remember { mutableStateOf("main") } - - trackNavigationChangesAsync(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - - val isInPrescriptionScreen by remember { - derivedStateOf { - currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route - } - } - - val onClickGoToInvoicesAction = { - mainNavController.navigate(PkvRoutes.InvoiceListScreen.path(activeProfile.id)) - } - - HandleConsentState( - consentState = consentState, - snackbar = snackbar, - onClickSnackbarAction = onClickGoToInvoicesAction - ) - HandleConsentErrorState( - consentErrorState = consentErrorState, - onRetry = { - consentController.grantChargeConsent() - }, - onClickToInvoices = onClickGoToInvoicesAction, - onShowCardWall = { - mainNavController.navigate(CardWallRoutes.CardWallIntroScreen.path(activeProfile.id)) - } - ) - - MainScreenSnackbar( - mainScreenController = mainScreenController, - scaffoldState = scaffoldState - ) - - OrderSuccessHandler(mainScreenController) - - if (sheetState.currentValue != ModalBottomSheetValue.Hidden) { - DisposableEffect(Unit) { - onDispose { - when (mainScreenBottomSheetContentState) { - is MainScreenBottomSheetContentState.Welcome -> { - mainScreenController.welcomeDrawerShown() - if (showToolTips) { - startToolTips = true - } - } - - is MainScreenBottomSheetContentState.GrantConsent -> { - mainScreenController.giveConsentDrawerShown(activeProfile.id) - } - - else -> {} - } - } - } - } - - LaunchedEffect(mainScreenBottomSheetContentState) { - if (mainScreenBottomSheetContentState != null) { - sheetState.show() - } else { - sheetState.hide() - } - } - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - mainScreenBottomSheetContentState?.let { analytics.trackMainScreenBottomPopUps(it) } - } else { - analytics.onPopUpClosed() - val route = Uri.parse(mainNavController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - - if (showWelcomeDrawer) { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() - mainScreenController.welcomeDrawerShown() - } - - if (showGetConsentDrawer) { - startGetConsentBottomSheet = true - mainScreenController.giveConsentDrawerShown(activeProfile.id) - } - - if (startGetConsentBottomSheet && isInPrescriptionScreen) { - startGetConsentBottomSheet = false - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.GrantConsent() - } - - LaunchedEffect(Unit) { - val accessibilityManager = - context.getSystemService(Context.ACCESSIBILITY_SERVICE) as android.view.accessibility.AccessibilityManager - - if (accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN) - .isNotEmpty() - ) { - mainScreenController.toolTipsShown() - } - } - var profileToRename by remember { - mutableStateOf(DEFAULT_EMPTY_PROFILE) - } - val toolTipBounds = remember { - mutableStateOf>(emptyMap()) - } - if (startToolTips) { - ToolTips( - isInPrescriptionScreen, - toolTipBounds - ) { - startToolTips = false - mainScreenController.toolTipsShown() - } - } - - val coroutineScope = rememberCoroutineScope() - BackHandler(enabled = sheetState.isVisible) { - coroutineScope.launch { - sheetState.hide() - mainScreenBottomSheetContentState = null - } - } - ModalBottomSheetLayout( - sheetState = sheetState, - modifier = Modifier - .imePadding() - .testTag(TestTag.Main.MainScreenBottomSheet.Modal), - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) }, - sheetContent = { - MainScreenBottomSheetContentState( - mainNavController = mainNavController, - mainScreenController = mainScreenController, - profileController = profileController, - infoContentState = mainScreenBottomSheetContentState, - profileToRename = profileToRename, - onGrantConsent = { - coroutineScope.launch { - sheetState.hide() - } - dialog.show { dialog -> - GrantConsentDialog( - onGrantConsent = { - consentController.grantChargeConsent() - dialog.dismiss() - }, - onCancel = { - dialog.dismiss() - } - ) - } - }, - onCancel = { - coroutineScope.launch { - sheetState.hide() - } - } - ) - } - ) { - // TODO: move to general place? - ExternalAuthenticationDialog() - MainScreenScaffold( - mainScreenController = mainScreenController, - profileController = profileController, - mainNavController = mainNavController, - bottomNavController = bottomNavController, - showToolTipps = startToolTips, - tooltipBounds = toolTipBounds, - onClickAddProfile = { - mainScreenBottomSheetContentState = - MainScreenBottomSheetContentState.AddProfile() - }, - onClickChangeProfileName = { profile -> - profileToRename = profile - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfileName() - }, - onClickAvatar = { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfilePicture() - }, - onClickAddPrescription = onClickAddPrescription, - scaffoldState = scaffoldState - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt deleted file mode 100644 index 520fb442..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.material.ScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.ui.RefreshedState -import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource -import java.net.HttpURLConnection - -@Composable -fun MainScreenSnackbar( - mainScreenController: MainScreenController, - scaffoldState: ScaffoldState -) { - var refreshEvent by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { - mainScreenController.onRefreshEvent.collect { - refreshEvent = it - } - } - - val refreshEventText = refreshEvent?.let { - when (it) { - GeneralErrorState.NetworkNotAvailable -> - stringResource(R.string.error_message_network_not_available) - is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - if (it.code != HttpURLConnection.HTTP_GONE && it.code != HttpURLConnection.HTTP_NOT_FOUND) { - stringResource(R.string.error_message_server_communication_failed).format(it.code) - } else { - stringResource(R.string.zero_prescriptions_updatet) - } - is GeneralErrorState.FatalTruststoreState -> - stringResource(R.string.error_message_vau_error) - is RefreshedState -> { - if (it.nrOfNewPrescriptions == 0) { - stringResource(R.string.zero_prescriptions_updatet) - } else { - annotatedPluralsResource( - R.plurals.prescriptions_updated, - quantity = it.nrOfNewPrescriptions, - AnnotatedString(it.nrOfNewPrescriptions.toString()) - ) - } - } - else -> "" - } - } - - LaunchedEffect(refreshEventText) { - try { - refreshEventText?.let { - scaffoldState.snackbarHostState.showSnackbar(refreshEventText.toString()) - } - } finally { - refreshEvent = null - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt index cf1b8b1e..99a33d65 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.AppBarDefaults import androidx.compose.material.Icon @@ -32,108 +33,79 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AddCircle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.animated.AnimationTime import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.profiles.presentation.ProfileController +import de.gematik.ti.erp.app.mainscreen.model.MultiProfileAppBarFlowWrapper +import de.gematik.ti.erp.app.mainscreen.ui.components.AddProfileChip import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.TopAppBarWithContent import kotlinx.coroutines.delay -/** - * The top appbar of the actual main screen. - */ @Composable internal fun MultiProfileTopAppBar( - mainScreenController: MainScreenController, - profileController: ProfileController, - isInPrescriptionScreen: Boolean, + multiProfileData: MultiProfileAppBarFlowWrapper, elevated: Boolean, - onClickAddProfile: () -> Unit, - onClickChangeProfileName: (profile: Profile) -> Unit, onClickAddPrescription: () -> Unit, - showToolTipps: Boolean, - tooltipBounds: MutableState> + onClickChangeProfileName: (profile: Profile) -> Unit, + onClickAddProfile: () -> Unit, + switchActiveProfile: (Profile) -> Unit ) { - val profiles by profileController.getProfilesState() - val activeProfile by profileController.getActiveProfileState() val accScan = stringResource(R.string.main_scan_acc) - val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else 0.dp } + val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else SizeDefaults.zero } TopAppBarWithContent( title = { - MainScreenTopBarTitle(isInPrescriptionScreen) + MainScreenTopBarTitle() }, elevation = elevation, backgroundColor = AppTheme.colors.neutral025, actions = @Composable { - if (isInPrescriptionScreen) { - // data matrix code scanner - IconButton( - onClick = onClickAddPrescription, - modifier = Modifier - .testTag("erx_btn_scn_prescription") - .semantics { contentDescription = accScan } - .onGloballyPositioned { coordinates -> - if (showToolTipps) { - tooltipBounds.value += Pair(0, coordinates.boundsInRoot()) - } - } - ) { - Icon( - imageVector = Icons.Rounded.AddCircle, - contentDescription = null, - tint = AppTheme.colors.primary700, - modifier = Modifier.size(24.dp) - ) - } + // data matrix code scanner + IconButton( + onClick = onClickAddPrescription, + modifier = Modifier + .testTag("erx_btn_scn_prescription") + .semantics { contentDescription = accScan } + ) { + Icon( + imageVector = Icons.Rounded.AddCircle, + contentDescription = null, + tint = AppTheme.colors.primary700, + modifier = Modifier.size(SizeDefaults.triple) + ) } }, content = { ProfilesChipBar( - mainScreenController = mainScreenController, - profiles = profiles, - activeProfile = activeProfile, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = showToolTipps, + multiProfileData = multiProfileData, onClickChangeProfileName = onClickChangeProfileName, onClickAddProfile = onClickAddProfile, - onClickChangeActiveProfile = { profile -> - profileController.switchActiveProfile(profile.id) - } + onClickChangeActiveProfile = switchActiveProfile ) } ) } @Composable -private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { - val text = if (isInPrescriptionScreen) { - stringResource(R.string.pres_bottombar_prescriptions) - } else { - stringResource(R.string.orders_title) - } +private fun MainScreenTopBarTitle() { Text( - text = text, + text = stringResource(R.string.pres_bottombar_prescriptions), style = AppTheme.typography.h5, maxLines = 1, overflow = TextOverflow.Ellipsis @@ -142,58 +114,51 @@ private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { @Composable private fun ProfilesChipBar( - mainScreenController: MainScreenController, - profiles: List, - activeProfile: Profile, - tooltipBounds: MutableState>, - toolTipBoundsRequired: Boolean, + multiProfileData: MultiProfileAppBarFlowWrapper, onClickChangeActiveProfile: (Profile) -> Unit, onClickChangeProfileName: (profile: Profile) -> Unit, onClickAddProfile: () -> Unit ) { val rowState = rememberLazyListState() - var indexOfActiveProfile by remember { mutableStateOf(0) } + // flows are collected inside the composable to avoid recomposition when the states change + val activeProfile by multiProfileData.activeProfile.collectAsStateWithLifecycle() + val profiles by multiProfileData.existingProfiles.collectAsStateWithLifecycle() + val refreshState by multiProfileData.isProfileRefreshing.collectAsStateWithLifecycle() + + val indexOfActiveProfile by remember(multiProfileData.existingProfiles, multiProfileData.activeProfile) { + mutableIntStateOf(profiles.indexOfFirst { it.id == activeProfile.id }.plus(1)) + } LaunchedEffect(indexOfActiveProfile) { - delay(timeMillis = 300L) + delay(timeMillis = AnimationTime.SHORT_L) rowState.animateScrollToItem(indexOfActiveProfile) } LazyRow( - state = rowState, - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), modifier = Modifier .fillMaxWidth() .padding(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Small), + state = rowState, + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), verticalAlignment = Alignment.CenterVertically ) { item { SpacerSmall() } - profiles.forEachIndexed { index, profile -> - if (profile.id == activeProfile.id) { - indexOfActiveProfile = index + 1 - } - - item { - ProfileChip( - profile = profile, - mainScreenController = mainScreenController, - selected = profile.id == activeProfile.id, - onClickChip = onClickChangeActiveProfile, - onClickChangeProfileName = onClickChangeProfileName, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired - ) - SpacerSmall() - } + items(profiles) { profile -> + ProfileChip( + profile = profile, + selected = profile.id == activeProfile.id, + refreshState = refreshState, + onClickChangeProfileName = onClickChangeProfileName, + onClickChip = onClickChangeActiveProfile + ) + SpacerSmall() } item { AddProfileChip( - onClickAddProfile = onClickAddProfile, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired + onClickAddProfile = onClickAddProfile ) SpacerMedium() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderStateChangeSideEffect.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderStateChangeSideEffect.kt new file mode 100644 index 00000000..293834dc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderStateChangeSideEffect.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import android.app.Activity +import android.content.Context +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.SnackbarResult +import com.google.android.play.core.review.ReviewManagerFactory +import de.gematik.ti.erp.app.mainscreen.presentation.AppController +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Suppress("FunctionNaming") +fun OrderStateChangeOnSuccessSideEffect( + context: Context, + snackbar: SnackbarHostState, + scope: CoroutineScope, + orderedEvent: AppController.OrderedEvent?, + resetOrdered: () -> Unit +) { + if (orderedEvent == AppController.OrderedEvent.Success) { + scope.requestReview(context) { + it.launch { + val result = snackbar.showSnackbar( + message = "Review is requested here in release mode", + actionLabel = "OK" + ) + when (result) { + SnackbarResult.Dismissed -> snackbar.currentSnackbarData?.dismiss() + SnackbarResult.ActionPerformed -> snackbar.currentSnackbarData?.dismiss() + } + } + } + resetOrdered() + } +} + +private fun CoroutineScope.requestReview( + context: Context, + inDebugMode: (CoroutineScope) -> Unit +) { + try { + val manager = ReviewManagerFactory.create(context) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + manager.launchReviewFlow(context as Activity, reviewInfo) + } + } + } catch (e: Throwable) { + Napier.e { "error on request review ${e.stackTraceToString()}" } + } + if (BuildConfigExtension.isDebugOrMinifiedDebug) { + inDebugMode(this) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt deleted file mode 100644 index 8705b9df..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import android.app.Activity -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.google.android.play.core.review.ReviewManagerFactory -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.utils.compose.AcceptDialog - -@Composable -fun OrderSuccessHandler( - mainScreenController: MainScreenController -) { - val context = LocalContext.current - - when (mainScreenController.orderedEvent) { - MainScreenController.OrderedEvent.Success -> { - LaunchedEffect(Unit) { - requestReview(context) - mainScreenController.resetOrderedEvent() - } - } - - MainScreenController.OrderedEvent.Error -> { - AcceptDialog( - header = stringResource(R.string.pharmacy_order_not_possible_title), - info = stringResource(R.string.pharmacy_order_not_possible_desc), - acceptText = stringResource(R.string.ok), - onClickAccept = { - mainScreenController.resetOrderedEvent() - } - ) - } - - null -> { - // noop - } - } -} - -private fun requestReview(context: Context) { - val manager = ReviewManagerFactory.create(context) - val request = manager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - val reviewInfo = task.result - manager.launchReviewFlow(context as Activity, reviewInfo) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChip.kt new file mode 100644 index 00000000..e568c8fe --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChip.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Autorenew +import androidx.compose.material.icons.outlined.CloudOff +import androidx.compose.material.icons.rounded.CloudDone +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults + +private const val OneThird = 1 / 3f +private const val IconTweenDuration = 2000 + +@Suppress("CyclomaticComplexMethod") +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ProfileChip( + profile: Profile, + refreshState: Boolean, + selected: Boolean, + onClickChangeProfileName: (profile: Profile) -> Unit, + onClickChip: (Profile) -> Unit +) { + val haptic = LocalHapticFeedback.current + + val ssoTokenScope = profile.ssoTokenScope + + data class IconState(val isVisible: Boolean, val icon: ImageVector) + + val isRefreshing = remember(refreshState, ssoTokenScope?.token) { refreshState } + + // Determine the icon visibility and type based on refreshState and token validity + val iconState by remember(refreshState, ssoTokenScope?.token) { + derivedStateOf { + when { + isRefreshing -> IconState(true, Icons.Outlined.Autorenew) + ssoTokenScope?.token?.isValid() == true -> IconState(true, Icons.Rounded.CloudDone) + else -> IconState(true, Icons.Outlined.CloudOff) + } + } + } + + val color = ssoStatusColor(isRefreshing, profile, ssoTokenScope) ?: AppTheme.colors.neutral400 + + val configuration = LocalConfiguration.current + val maxChipWidth = (configuration.screenWidthDp.dp) * OneThird + + val shape = RoundedCornerShape(SizeDefaults.one) + + val backgroundColor = if (selected) { + AppTheme.colors.neutral100 + } else { + AppTheme.colors.neutral025 + } + val textColor = if (selected) { + AppTheme.colors.neutral900 + } else { + AppTheme.colors.neutral600 + } + val borderColor = if (selected) { + AppTheme.colors.neutral300 + } else { + AppTheme.colors.neutral200 + } + + val description = stringResource(R.string.mainscreen_profile_chip_content_description) + + Surface( + modifier = Modifier + .clip(shape) + .combinedClickable( + onClick = { + onClickChip(profile) + }, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onClickChangeProfileName(profile) + }, + role = Role.Button + ) + .widthIn(max = maxChipWidth) + .width(IntrinsicSize.Max) + .semantics { + contentDescription = description + }, + shape = shape, + border = BorderStroke(SizeDefaults.eighth, borderColor), + color = backgroundColor + ) { + Row( + modifier = Modifier.padding(vertical = SizeDefaults.one, horizontal = PaddingDefaults.ShortMedium), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SizeDefaults.one) + ) { + Text( + text = profile.name, + style = AppTheme.typography.subtitle2, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + if (profile.lastAuthenticated != null && selected) { + val animatedIconSize by animateDpAsState( + label = "iconSize", + targetValue = if (isRefreshing) SizeDefaults.doubleHalf else SizeDefaults.double, + animationSpec = tween(durationMillis = IconTweenDuration) + ) + val animatedAlpha by animateFloatAsState( + label = "iconAlpha", + targetValue = if (iconState.isVisible) 1f else 0.7f, + animationSpec = tween(durationMillis = IconTweenDuration) + ) + val rotateAlpha by rememberInfiniteTransition(label = "rotate").animateFloat( + label = "shimmerAlpha", + initialValue = 0f, + targetValue = 359f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + easing = FastOutLinearInEasing + ), + repeatMode = RepeatMode.Restart + ) + ) + val shimmerAlpha by rememberInfiniteTransition(label = "shimmer").animateFloat( + label = "shimmerAlpha", + initialValue = 0.5f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + easing = LinearEasing + ), + repeatMode = RepeatMode.Reverse + ) + ) + + Icon( + modifier = Modifier + .size(animatedIconSize) + .then( + when { + isRefreshing -> Modifier.rotate(rotateAlpha) + else -> Modifier + } + ) + .graphicsLayer { + alpha = if (isRefreshing) shimmerAlpha else animatedAlpha + }, + imageVector = iconState.icon, + contentDescription = null, + tint = color + ) + } + } + } +} + +@Composable +private fun ssoStatusColor(isRefreshing: Boolean, profile: Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = + when { + isRefreshing -> AppTheme.colors.primary400 + ssoTokenScope?.token?.isValid() == true -> AppTheme.colors.green400 + profile.lastAuthenticated != null -> AppTheme.colors.neutral400 + else -> null + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt deleted file mode 100644 index ff780241..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CloudOff -import androidx.compose.material.icons.rounded.CloudDone -import androidx.compose.material.icons.rounded.PersonAdd -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.presentation.rememberRefreshPrescriptionsController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import kotlinx.coroutines.delay - -private const val OneThird = 1 / 3f -private const val StateVisibilityTime = 4000L -private const val IconTweenDuration = 2000 - -@Composable -fun AddProfileChip( - onClickAddProfile: () -> Unit, - tooltipBounds: MutableState>, - toolTipBoundsRequired: Boolean -) { - val shape = RoundedCornerShape(8.dp) - - Surface( - modifier = Modifier - .clip(shape) - .clickable { - onClickAddProfile() - } - .height(IntrinsicSize.Max) - .onGloballyPositioned { coordinates -> - if (toolTipBoundsRequired) { - tooltipBounds.value += Pair(2, coordinates.boundsInRoot()) - } - } - .testTag(TestTag.Main.AddProfileButton), - shape = shape, - border = BorderStroke(1.dp, AppTheme.colors.neutral300) - ) { - Row( - modifier = Modifier.padding(vertical = 6.dp, horizontal = PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Rounded.PersonAdd, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = AppTheme.colors.primary600 - ) - } - // empty text to achieve same height as profile chips - Text( - text = "", - style = AppTheme.typography.subtitle2, - modifier = Modifier.padding(vertical = 8.dp, horizontal = PaddingDefaults.ShortMedium) - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ProfileChip( - profile: Profile, - mainScreenController: MainScreenController, - tooltipBounds: MutableState>, - selected: Boolean, - toolTipBoundsRequired: Boolean, - onClickChangeProfileName: (profile: Profile) -> Unit, - onClickChip: (Profile) -> Unit -) { - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) - - val isRefreshing by refreshPrescriptionsController.isRefreshing - var refreshEvent by remember { mutableStateOf(null) } - - LaunchedEffect(Unit) { - mainScreenController.onRefreshEvent.collect { - refreshEvent = it - } - } - - var iconVisible by remember { mutableStateOf(false) } - val ssoTokenScope = profile.ssoTokenScope - - LaunchedEffect(Unit) { - ssoTokenScope?.token?.let { - if (!it.isValid()) { - iconVisible = true - delay(StateVisibilityTime) - iconVisible = false - } - } - } - - LaunchedEffect(key1 = refreshEvent, key2 = isRefreshing) { - iconVisible = true - delay(StateVisibilityTime) - iconVisible = false - } - - val icon = when { - ssoTokenScope?.token?.isValid() == true -> Icons.Rounded.CloudDone - else -> Icons.Outlined.CloudOff - } - - val color = if (refreshEvent is PrescriptionServiceErrorState) { - AppTheme.colors.neutral600 - } else { - ssoStatusColor(profile, ssoTokenScope) ?: AppTheme.colors.neutral400 - } - - val configuration = LocalConfiguration.current - val maxChipWidth = (configuration.screenWidthDp.dp) * OneThird - - val shape = RoundedCornerShape(8.dp) - - val backgroundColor = if (selected) { - AppTheme.colors.neutral100 - } else { - AppTheme.colors.neutral025 - } - val textColor = if (selected) { - AppTheme.colors.neutral900 - } else { - AppTheme.colors.neutral600 - } - val borderColor = if (selected) { - AppTheme.colors.neutral300 - } else { - AppTheme.colors.neutral200 - } - - val description = stringResource(R.string.mainscreen_profile_chip_content_description) - - Surface( - modifier = Modifier - .clip(shape) - .combinedClickable( - onClick = { onClickChip(profile) }, - onLongClick = { onClickChangeProfileName(profile) }, - role = Role.Button - ) - .widthIn(max = maxChipWidth) - .width(IntrinsicSize.Max) - .semantics { - contentDescription = description - } - .onGloballyPositioned { coordinates -> - if (profile.active && toolTipBoundsRequired) { - tooltipBounds.value += Pair(1, coordinates.boundsInRoot()) - } - }, - shape = shape, - border = BorderStroke(1.dp, borderColor), - color = backgroundColor - ) { - Row( - modifier = Modifier - .padding(vertical = 8.dp, horizontal = PaddingDefaults.ShortMedium), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = profile.name, - style = AppTheme.typography.subtitle2, - color = textColor, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - - if (profile.lastAuthenticated != null && selected) { - AnimatedVisibility( - visible = iconVisible, - enter = fadeIn(animationSpec = tween(IconTweenDuration)), - exit = fadeOut(animationSpec = tween(IconTweenDuration)) - ) { - Icon( - imageVector = icon, - modifier = Modifier - .size(16.dp), - contentDescription = null, - tint = color - ) - } - } - } - } -} - -@Composable -private fun ssoStatusColor(profile: Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = - when { - ssoTokenScope?.token?.isValid() == true -> AppTheme.colors.green400 - profile.lastAuthenticated != null -> AppTheme.colors.neutral400 - else -> null - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt index fb2ad684..2328a5b2 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt @@ -1,131 +1,209 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.input.TextFieldValue import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.presentation.ProfileController +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge +import de.gematik.ti.erp.app.utils.extensions.isKeyboardVisible import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName -import kotlinx.coroutines.launch +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty +import kotlinx.coroutines.android.awaitFrame -@OptIn(ExperimentalComposeUiApi::class) +/** + * @param key: UUID is used to differentiate between different instance of the same profile for the bottomsheet + * @param profileToEdit is used to get the profile to edit + * @param existingProfiles: List is used to get the list of existing profiles + * @param keyboardController: SoftwareKeyboardController is used to control the keyboard + * @param addProfile: Boolean is used to differentiate between adding a new profile and editing an existing one + * @param onCancel: () -> Unit is used to dismiss the bottomsheet + */ +// todo : make this to a bottom-sheet screen +@Suppress("CyclomaticComplexMethod") @Composable fun ProfileSheetContent( - profileController: ProfileController, + key: String, + existingProfiles: List, profileToEdit: ProfilesUseCaseData.Profile?, + keyboardController: SoftwareKeyboardController?, addProfile: Boolean = false, + onUpdate: (ProfileIdentifier, String) -> Unit, + onAdd: (String) -> Unit, onCancel: () -> Unit ) { - val keyboardController = LocalSoftwareKeyboardController.current - val scope = rememberCoroutineScope() - val profilesState by profileController.getProfilesState() - var textValue by remember { mutableStateOf(profileToEdit?.name ?: "") } - var duplicated by remember { mutableStateOf(false) } + val view = LocalView.current + val focusRequester = remember(key, profileToEdit) { FocusRequester() } + var textValue by remember(key, profileToEdit) { + mutableStateOf( + TextFieldValue( + text = profileToEdit?.name ?: "", + selection = TextRange((profileToEdit?.name ?: "").length) + ) + ) + } + var duplicated by remember(key, profileToEdit) { mutableStateOf(false) } + // if the textValue is empty and the profileToEdit is null, then it is a new profile on start + var isNewProfile by remember(key) { mutableStateOf(textValue.text.isEmpty()) } - val onEdit = { - if (!addProfile) { - profileToEdit?.let { - scope.launch { profileController.updateProfileName(it.id, textValue) } - } + val showError by remember(duplicated, textValue, isNewProfile) { + mutableStateOf(duplicated || textValue.text.isEmpty() && !isNewProfile) + } + + val onSave = { + if (addProfile) { + onAdd(textValue.text) } else { - scope.launch { - profileController.addProfile(textValue) + profileToEdit?.let { + onUpdate(it.id, textValue.text) } } onCancel() keyboardController?.hide() } + + LaunchedEffect(key, profileToEdit) { + focusRequester.requestFocus() + if (!view.isKeyboardVisible) { + awaitFrame() + keyboardController?.show() + } + } + Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - OutlinedTextField( - modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField), - shape = RoundedCornerShape(8.dp), + ErezeptOutlineText( + modifier = Modifier + .testTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) + .focusRequester(focusRequester), + shape = RoundedCornerShape(SizeDefaults.one), value = textValue, singleLine = true, - onValueChange = { - val isNotExistingText = textValue.trim() != profileToEdit?.name - val isNotExistingName = profilesState.containsProfileWithName(textValue) - val name = it.trimStart().sanitizeProfileName() - textValue = name - duplicated = isNotExistingText && isNotExistingName + onValueChange = { changedValue -> + isNewProfile = false // once the user starts typing, it is no longer a new profile for this composable + textValue = TextFieldValue( + text = changedValue.text.sanitizeProfileName(), + selection = changedValue.selection, + composition = changedValue.composition + ) + val isExistingText = textValue.text.trim() == profileToEdit?.name?.trim() + val isExistingProfileName = existingProfiles.containsProfileWithName(textValue.text) + duplicated = isExistingProfileName || isExistingText }, keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, imeAction = ImeAction.Done, capitalization = KeyboardCapitalization.Sentences ), keyboardActions = KeyboardActions { - if (!duplicated && textValue.isNotEmpty()) { - onEdit() + if (!duplicated && textValue.text.isNotEmpty()) { + onSave() } }, placeholder = { Text(stringResource(R.string.profile_edit_name_place_holder)) }, - isError = duplicated + isError = showError, + trailingIcon = { + Crossfade( + label = "", + targetState = textValue.text.isNotEmpty() + ) { expectedState -> + if (expectedState) { + IconButton( + onClick = { + textValue = TextFieldValue( + text = "", + selection = TextRange(0) + ) + duplicated = false + } + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + } + } + } ) - - if (duplicated) { + AnimatedVisibility(showError) { Text( - stringResource(R.string.edit_profile_duplicated_profile_name), + text = when { + textValue.text.isNotNullOrEmpty() && duplicated -> stringResource( + R.string.edit_profile_duplicated_profile_name + ) + + else -> stringResource(id = R.string.edit_profile_empty_profile_name) + }, color = AppTheme.colors.red600, style = AppTheme.typography.caption1, - modifier = Modifier.padding(start = PaddingDefaults.Medium) + modifier = Modifier.padding( + top = PaddingDefaults.Medium, + start = PaddingDefaults.Medium + ) ) } SpacerLarge() PrimaryButton( modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton), - enabled = !duplicated && textValue.isNotEmpty(), + enabled = !duplicated && textValue.text.isNotEmpty(), onClick = { - onEdit() + onSave() } ) { Text(stringResource(R.string.profile_bottom_sheet_save)) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt deleted file mode 100644 index 86440db5..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExtendedFloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Upload -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.redeem.ui.rememberRedeemController -import de.gematik.ti.erp.app.utils.compose.AcceptDialog - -@Composable -fun RedeemFloatingActionButton( - activeProfile: ProfilesUseCaseData.Profile, - onClick: () -> Unit -) { - val redeemState = rememberRedeemController(activeProfile) - val hasRedeemableTasks by redeemState.hasRedeemableTasks - - var showNoRedeemableDialog by remember { mutableStateOf(false) } - if (showNoRedeemableDialog) { - AcceptDialog( - header = stringResource(R.string.main_redeem_no_rx_title), - info = stringResource(R.string.main_redeem_no_rx_desc), - acceptText = stringResource(R.string.ok), - onClickAccept = { - showNoRedeemableDialog = false - } - ) - } - - ExtendedFloatingActionButton( - modifier = Modifier.heightIn(min = 56.dp), - text = { Text(stringResource(R.string.main_redeem_button)) }, - shape = RoundedCornerShape(16.dp), - icon = { Icon(Icons.Rounded.Upload, null) }, - onClick = { - if (hasRedeemableTasks) { - onClick() - } else { - showNoRedeemableDialog = true - } - } - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemFloatingActionButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemFloatingActionButton.kt new file mode 100644 index 00000000..3bd5c4a3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemFloatingActionButton.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Upload +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun RedeemFloatingActionButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + ExtendedFloatingActionButton( + modifier = Modifier + .then(modifier) + .heightIn(min = SizeDefaults.sevenfold) + .systemBarsPadding(), + text = { Text(stringResource(R.string.main_redeem_button)) }, + shape = RoundedCornerShape(SizeDefaults.double), + icon = { Icon(Icons.Rounded.Upload, null) }, + onClick = onClick + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt index b0ba9874..2bfe1f94 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -29,8 +29,9 @@ import androidx.compose.ui.Modifier import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefreshIndicator import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.AppController import de.gematik.ti.erp.app.prescription.presentation.rememberRefreshPrescriptionsController +import de.gematik.ti.erp.app.prescription.usecase.RefreshState import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme import kotlinx.coroutines.Dispatchers @@ -43,17 +44,22 @@ private const val SpinnerDelay = 300L fun RefreshScaffold( profileId: ProfileIdentifier, onUserNotAuthenticated: () -> Unit, - mainScreenController: MainScreenController, + appController: AppController, onShowCardWall: () -> Unit, content: @Composable (onRefresh: (isUserAction: Boolean, priority: MutatePriority) -> Unit) -> Unit ) { val scope = rememberCoroutineScope() val mutex = MutatorMutex() - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) + val refreshPrescriptionsController = rememberRefreshPrescriptionsController(appController) val isRefreshing by refreshPrescriptionsController.isRefreshing - val refreshState = rememberSwipeRefreshState(isRefreshing) + val refreshState = rememberSwipeRefreshState( + isRefreshing = when (isRefreshing) { + RefreshState.InProgress -> true + else -> false + } + ) suspend fun refresh( isUserAction: Boolean, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt deleted file mode 100644 index b0139201..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import androidx.compose.ui.window.Dialog -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults - -enum class ArrowPosition { - Top, - Right -} - -@Immutable -data class ToolTipState( - var arrowPosition: ArrowPosition = ArrowPosition.Top, - var elementBound: Rect = Rect(0f, 0f, 0f, 0f) -) - -@Composable -fun ToolTips( - isInPrescriptionScreen: Boolean, - toolTipBounds: MutableState>, - onToolTipsShown: () -> Unit -) { - var tooltipNr by remember { mutableStateOf(0) } - - if (isInPrescriptionScreen) { - when (tooltipNr) { - 0 -> ToolTip( - onDismissRequest = { - tooltipNr += 1 - }, - tooltipState = ToolTipState( - arrowPosition = ArrowPosition.Right, - elementBound = toolTipBounds.value[0] ?: Rect.Zero - ), - content = stringResource(R.string.main_screen_tooltip_1_content) - ) - - 1 -> ToolTip( - onDismissRequest = { - tooltipNr += 1 - }, - tooltipState = ToolTipState( - arrowPosition = ArrowPosition.Top, - elementBound = toolTipBounds.value[1] ?: Rect.Zero - ), - content = stringResource(R.string.main_screen_tooltip_2_content) - ) - - 2 -> ToolTip( - onDismissRequest = onToolTipsShown, - tooltipState = ToolTipState( - arrowPosition = ArrowPosition.Top, - elementBound = toolTipBounds.value[2] ?: Rect.Zero - ), - content = stringResource(R.string.main_screen_tooltip_3_content) - ) - } - } -} - -@Composable -fun ToolTip( - onDismissRequest: () -> Unit, - tooltipState: ToolTipState, - content: String -) { - val contentDescription = stringResource(R.string.main_screen_tooltip_tap_screen) - Dialog( - onDismissRequest = onDismissRequest - ) { - when (tooltipState.arrowPosition) { - ArrowPosition.Top -> ToolTipWithArrowTop(tooltipState, content, contentDescription, onDismissRequest) - ArrowPosition.Right -> ToolTipWithArrowRight(tooltipState, content, contentDescription, onDismissRequest) - } - } -} - -@Composable -fun ToolTipWithArrowTop( - tooltipState: ToolTipState, - content: String, - description: String, - onDismissRequest: () -> Unit -) { - val offset = with(LocalDensity.current) { - DpOffset( - x = tooltipState.elementBound.bottomCenter.x.toDp() - 12.dp, - y = tooltipState.elementBound.bottomCenter.y.toDp() - 36.dp - ) - } - - TooltipScaffold( - description = description, - onDismissRequest = onDismissRequest - ) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Icon( - painterResource(R.drawable.ic_tooltip_arrow_top), - null, - tint = AppTheme.colors.neutral800, - modifier = Modifier - .offset(x = offset.x, y = offset.y) - ) - val textAlignX = min(this@TooltipScaffold.maxWidth - 231.dp - PaddingDefaults.Large, offset.x - 16.dp) - Text( - text = content, - modifier = Modifier - .width(231.dp) - .offset(x = textAlignX, y = offset.y) - .background( - color = AppTheme.colors.neutral800, - shape = RoundedCornerShape(16.dp) - ) - .padding(horizontal = PaddingDefaults.Large, vertical = PaddingDefaults.Medium), - color = AppTheme.colors.neutral000, - style = AppTheme.typography.body2 - ) - } - } -} - -@Composable -fun ToolTipWithArrowRight( - tooltipState: ToolTipState, - content: String, - description: String, - onDismissRequest: () -> Unit -) { - val offset = with(LocalDensity.current) { - DpOffset( - x = tooltipState.elementBound.centerLeft.x.toDp() - 231.dp, - y = tooltipState.elementBound.centerLeft.y.toDp() - 50.dp - ) - } - - TooltipScaffold( - description = description, - onDismissRequest = onDismissRequest - ) { - Row( - modifier = Modifier.offset(x = offset.x) - ) { - Text( - text = content, - modifier = Modifier - .width(231.dp) - .offset(y = offset.y - 16.dp) - .background( - color = AppTheme.colors.neutral800, - shape = RoundedCornerShape(16.dp) - ) - .padding(horizontal = PaddingDefaults.Large, vertical = PaddingDefaults.Medium), - color = AppTheme.colors.neutral000, - style = AppTheme.typography.body2 - ) - - Icon( - painter = painterResource(id = R.drawable.ic_tooltip_arrow_right), - contentDescription = null, - tint = AppTheme.colors.neutral800, - modifier = Modifier - .offset(y = offset.y) - ) - } - } -} - -@Composable -private fun TooltipScaffold( - description: String, - onDismissRequest: () -> Unit, - content: @Composable BoxWithConstraintsScope.() -> Unit -) = - BoxWithConstraints( - modifier = Modifier - .fillMaxSize() - .clickable( - onClick = onDismissRequest, - role = Role.Button, - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) - .semantics { contentDescription = description }, - content = content - ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt index 60d501bb..8c5dbc12 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/UniversalLinkWrongDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/UniversalLinkWrongDialog.kt index 3d12b718..4147321f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/UniversalLinkWrongDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/UniversalLinkWrongDialog.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mainscreen.ui @@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable fun UniversalLinkWrongDialog( @@ -34,7 +34,6 @@ fun UniversalLinkWrongDialog( body = stringResource(id = R.string.main_fasttrack_error_info), // okText = stringResource(R.string.cancel), // confirmText = stringResource(R.string.ok), - onConfirmRequest = onDismissRequest, onDismissRequest = onDismissRequest ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/AddProfileChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/AddProfileChip.kt new file mode 100644 index 00000000..9e5f8fa1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/AddProfileChip.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PersonAdd +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun AddProfileChip( + onClickAddProfile: () -> Unit +) { + val shape = RoundedCornerShape(SizeDefaults.one) + + Surface( + modifier = Modifier + .clip(shape) + .clickable { + onClickAddProfile() + } + .height(IntrinsicSize.Max) + .testTag(TestTag.Main.AddProfileButton), + shape = shape, + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral300) + ) { + Row( + modifier = Modifier.padding(vertical = SizeDefaults.threeQuarter, horizontal = PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Rounded.PersonAdd, + contentDescription = null, + modifier = Modifier.size(SizeDefaults.triple), + tint = AppTheme.colors.primary600 + ) + } + // empty text to achieve same height as profile chips + Text( + text = "", + style = AppTheme.typography.subtitle2, + modifier = Modifier.padding(vertical = SizeDefaults.one, horizontal = PaddingDefaults.ShortMedium) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/EditProfilePicture.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/EditProfilePicture.kt new file mode 100644 index 00000000..7c22ab37 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/EditProfilePicture.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.mainscreen.model.EditProfilePictureSelectionState +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.ui.components.ProfileBackgroundColorComponent +import de.gematik.ti.erp.app.profiles.ui.components.showProfileImageSelectorDialog +import de.gematik.ti.erp.app.profiles.ui.screens.AvatarPicker +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileImage +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.CenterColumn +import de.gematik.ti.erp.app.utils.extensions.LocalDialog + +@Suppress("ComplexMethod") +@Composable +internal fun EditProfilePicture( + profile: ProfilesUseCaseData.Profile, + clearPersonalizedImage: () -> Unit, + onPickImage: (EditProfilePictureSelectionState) -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit, + onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit +) { + val dialog = LocalDialog.current + var editableProfile by remember(profile.id) { mutableStateOf(profile) } + + Column(modifier = Modifier.fillMaxSize()) { + SpacerMedium() + ProfileImage( + selectedProfile = editableProfile, + onClickDeleteAvatar = { + editableProfile = editableProfile.copy( + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) + clearPersonalizedImage() + } + ) + + SpacerXXLarge() + AvatarPicker( + profile = editableProfile, + currentAvatar = editableProfile.avatar, + onPickPersonalizedImage = { + showProfileImageSelectorDialog( + dialogScaffold = dialog, + onPickEmojiImage = { + onPickImage(EditProfilePictureSelectionState.Emoji) + }, + onPickPersonalizedImage = { + onPickImage(EditProfilePictureSelectionState.PersonalizedImage) + }, + onPickCamera = { + onPickImage(EditProfilePictureSelectionState.Camera) + } + ) + }, + onSelectAvatar = { + editableProfile = editableProfile.copy(avatar = it) + onSelectAvatar(it) + } + ) + SpacerSmall() + CenterColumn { + ProfileBackgroundColorComponent( + color = editableProfile.color, + onColorPicked = { + editableProfile = editableProfile.copy(color = it) + onSelectProfileColor(it) + } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/RedeemNotPossibleDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/RedeemNotPossibleDialog.kt new file mode 100644 index 00000000..6ab91933 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/RedeemNotPossibleDialog.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mainscreen.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun RedeemNotPossibleDialog( + onClick: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.pharmacy_order_not_possible_title), + body = stringResource(R.string.pharmacy_order_not_possible_desc), + onDismissRequest = onClick + ) +} + +@LightDarkPreview +@Composable +fun RedeemNotPossibleDialogPreview() { + PreviewAppTheme { + RedeemNotPossibleDialog { + // noop + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/SheetStateOnChangeCallback.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/SheetStateOnChangeCallback.kt new file mode 100644 index 00000000..4137263f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/components/SheetStateOnChangeCallback.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UsingMaterialAndMaterial3Libraries") + +package de.gematik.ti.erp.app.mainscreen.ui.components + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect + +/** + * This composable is used to react when the bottom sheet is dismissed + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SheetStateOnChangeCallback( + sheetState: ModalBottomSheetState, + onOpen: () -> Unit, + onDismiss: () -> Unit +) { + DisposableEffect(sheetState.currentValue) { + onDispose { + if (sheetState.currentValue == ModalBottomSheetValue.Hidden) { + onDismiss() + } else { + onOpen() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ShowMedicationPlanSuccessObserver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ShowMedicationPlanSuccessObserver.kt new file mode 100644 index 00000000..81c7e2d4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ShowMedicationPlanSuccessObserver.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan + +import kotlinx.coroutines.flow.MutableStateFlow + +interface ShowMedicationPlanSuccessObserver { + val shouldShowMedicationSuccess: MutableStateFlow + fun shouldShowMedicationSuccess() + fun medicationSuccessHasBeenShown() + fun showMedicationSuccess(): Boolean +} + +class DefaultShowMedicationPlanSuccessScreenObserver : ShowMedicationPlanSuccessObserver { + override val shouldShowMedicationSuccess by lazy { MutableStateFlow(false) } + override fun shouldShowMedicationSuccess() { + shouldShowMedicationSuccess.value = true + } + override fun medicationSuccessHasBeenShown() { + shouldShowMedicationSuccess.value = false + } + override fun showMedicationSuccess(): Boolean = shouldShowMedicationSuccess.value +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/GetDayTimeImageAndDescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/GetDayTimeImageAndDescription.kt new file mode 100644 index 00000000..32058f0c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/GetDayTimeImageAndDescription.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.components + +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification + +@Suppress("MagicNumber") +fun getDayTimeImageAndDescription( + notification: MedicationNotification +): Pair { + val hour = notification.time.hour + + val resources = when (hour) { + in 4..10 -> R.drawable.morning to R.string.medication_plan_morning_text + in 10..15 -> R.drawable.noon to R.string.medication_plan_noon_text + in 15..17 -> R.drawable.afternoon to R.string.medication_plan_afternoon_text + else -> R.drawable.evening to R.string.medication_plan_evening_text + } + return resources +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanLineItem.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanLineItem.kt new file mode 100644 index 00000000..23b1a09e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanLineItem.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.utils.compose.Label + +@Composable +fun MedicationPlanLineItem( + medicationSchedule: MedicationSchedule?, + onClick: () -> Unit +) { + val text = when { + medicationSchedule == null -> stringResource(R.string.pres_details_schedule_medication_title_not_scheduled) + medicationSchedule.isActive -> stringResource(R.string.pres_details_schedule_medication_title_scheduled_active) + else -> stringResource(R.string.pres_details_schedule_medication_title_scheduled_not_active) + } + Label( + text = text, + label = stringResource(R.string.pres_details_schedule_medication_label), + onClick = onClick + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanReminderCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanReminderCard.kt new file mode 100644 index 00000000..f454f6e4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/components/MedicationPlanReminderCard.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny + +@Composable +fun MedicationPlanReminderCard( + modifier: Modifier = Modifier, + @DrawableRes imageResource: Int, + title: String, + description: String +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + colors = CardDefaults.cardColors().copy(containerColor = AppTheme.colors.neutral000), + border = BorderStroke(width = 1.dp, color = AppTheme.colors.neutral300) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + DayImage(imageResource = imageResource) + SpacerMedium() + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral900, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerTiny() + Text( + text = description, + style = AppTheme.typography.body2, + color = AppTheme.colors.neutral600, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +@Composable +private fun DayImage( + imageResource: Int +) { + Image( + painter = painterResource(id = imageResource), + contentDescription = null, + modifier = Modifier + .size(SizeDefaults.sixfold) + .clip(CircleShape) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/di/MedicationPlanModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/di/MedicationPlanModule.kt new file mode 100644 index 00000000..1b33684c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/di/MedicationPlanModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.di + +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanLocalDataSource +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.usecase.GetDosageInstructionByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.LoadAllMedicationSchedulesUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.LoadMedicationScheduleByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.LoadProfilesWithSchedulesUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.PlanMedicationScheduleUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.ScheduleReminderWorker +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val medicationPlanModule = DI.Module("medicationPlanModule") { + bindProvider { MedicationPlanLocalDataSource(instance()) } + bindProvider { MedicationPlanRepository(instance()) } + bindProvider { LoadAllMedicationSchedulesUseCase(instance()) } + bindProvider { LoadMedicationScheduleByTaskIdUseCase(instance()) } + bindProvider { GetDosageInstructionByTaskIdUseCase(instance()) } + bindProvider { LoadProfilesWithSchedulesUseCase(instance(), instance()) } + bindProvider { ScheduleReminderWorker(instance()) } + bindProvider { PlanMedicationScheduleUseCase(instance(), instance()) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanGraph.kt new file mode 100644 index 00000000..23038ba8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanGraph.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.medicationplan.ui.MedicationPlanDosageInfoBottomSheetScreen +import de.gematik.ti.erp.app.medicationplan.ui.MedicationPlanScheduleScreen +import de.gematik.ti.erp.app.medicationplan.ui.ScheduleDateRangeScreen +import de.gematik.ti.erp.app.medicationplan.ui.MedicationListScheduleScreen +import de.gematik.ti.erp.app.medicationplan.ui.components.MedicationNotificationSuccessScreen +import de.gematik.ti.erp.app.navigation.renderBottomSheet +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideInRight +import de.gematik.ti.erp.app.navigation.slideOutLeft +import de.gematik.ti.erp.app.navigation.slideOutUp + +fun NavGraphBuilder.medicationPlanGraph( + startDestination: String = MedicationPlanRoutes.MedicationPlanList.route, + navController: NavController +) { + navigation( + startDestination = startDestination, + route = MedicationPlanRoutes.subGraphName() + ) { + renderComposable( + route = MedicationPlanRoutes.MedicationPlanList.route, + arguments = MedicationPlanRoutes.MedicationPlanList.arguments, + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() } + ) { + MedicationListScheduleScreen( + navController = navController, + navBackStackEntry = it + ) + } + renderComposable( + route = MedicationPlanRoutes.MedicationPlanPerPrescription.route, + arguments = MedicationPlanRoutes.MedicationPlanPerPrescription.arguments, + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() } + ) { + MedicationPlanScheduleScreen( + navController = navController, + navBackStackEntry = it + ) + } + + renderComposable( + route = MedicationPlanRoutes.ScheduleDateRange.route, + arguments = MedicationPlanRoutes.ScheduleDateRange.arguments, + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() } + ) { + ScheduleDateRangeScreen( + navController = navController, + navBackStackEntry = it + ) + } + renderComposable( + route = MedicationPlanRoutes.MedicationPlanNotificationSuccess.route, + arguments = MedicationPlanRoutes.MedicationPlanNotificationSuccess.arguments + ) { + MedicationNotificationSuccessScreen( + navController = navController, + navBackStackEntry = it + ) + } + renderBottomSheet( + route = MedicationPlanRoutes.MedicationPlanDosageInfo.route, + arguments = MedicationPlanRoutes.MedicationPlanDosageInfo.arguments + ) { + MedicationPlanDosageInfoBottomSheetScreen( + navController = navController, + navBackStackEntry = it + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanRoutes.kt new file mode 100644 index 00000000..2ec3d67e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/navigation/MedicationPlanRoutes.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.navigation + +import androidx.navigation.NavType +import androidx.navigation.navArgument +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes + +object MedicationPlanRoutes : NavigationRoutes { + internal const val TASK_ID = "taskId" + override fun subGraphName() = "MedicationPlan" + + object MedicationPlanNotificationSuccess : Routes(NavigationRouteNames.SuccessScreen.name) + + object MedicationPlanList : Routes(NavigationRouteNames.ScheduleListScreen.name) + + object MedicationPlanPerPrescription : Routes( + NavigationRouteNames.ScheduleScreen.name, + navArgument(TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = path( + TASK_ID to taskId + ) + } + object MedicationPlanDosageInfo : Routes( + NavigationRouteNames.DosageInfoScreen.name, + navArgument(TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = path( + TASK_ID to taskId + ) + } + + object ScheduleDateRange : Routes( + NavigationRouteNames.ScheduleDateRangeScreen.name, + navArgument(TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = path( + TASK_ID to taskId + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenController.kt new file mode 100644 index 00000000..05113345 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenController.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.usecase.GetDosageInstructionByTaskIdUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +open class DosageInstructionBottomSheetScreenController( + private val getDosageInstructionByTaskIdUseCase: GetDosageInstructionByTaskIdUseCase, + private val taskId: String + +) : Controller() { + + private val _dosageInstruction = MutableStateFlow>(UiState.Loading()) + val dosageInstruction: StateFlow> = _dosageInstruction + + init { + controllerScope.launch { + runCatching { + getDosageInstructionByTaskIdUseCase(taskId).first() + }.onSuccess { + _dosageInstruction.value = UiState.Data(it) + }.onFailure { + _dosageInstruction.value = UiState.Error(it) + } + } + } +} + +@Composable +fun rememberDosageInstructionBottomSheetScreenController( + taskId: String +): DosageInstructionBottomSheetScreenController { + val getDosageInstructionByTaskIdUseCase by rememberInstance() + return remember(taskId) { + DosageInstructionBottomSheetScreenController( + getDosageInstructionByTaskIdUseCase = getDosageInstructionByTaskIdUseCase, + taskId = taskId + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenController.kt new file mode 100644 index 00000000..5650fa38 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenController.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.usecase.LoadProfilesWithSchedulesUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance +class MedicationListScheduleScreenController( + loadProfilesWithSchedulesUseCase: LoadProfilesWithSchedulesUseCase +) : Controller() { + private val _profilesWithSchedules: + MutableStateFlow>> = + MutableStateFlow(UiState.Loading()) + val profilesWithSchedules: StateFlow>> = + _profilesWithSchedules + + init { + controllerScope.launch { + runCatching { + loadProfilesWithSchedulesUseCase() + }.fold( + onSuccess = { profileWithSchedules -> + if (profileWithSchedules.first().isEmpty()) { + _profilesWithSchedules.value = UiState.Empty() + } else { + _profilesWithSchedules.value = UiState.Data(profileWithSchedules.first()) + } + }, + onFailure = { + _profilesWithSchedules.value = UiState.Error(it) + } + ) + } + } +} + +@Composable +fun rememberMedicationListScheduleScreenController(): MedicationListScheduleScreenController { + val loadProfilesWithSchedulesUseCase by rememberInstance() + return remember { + MedicationListScheduleScreenController( + loadProfilesWithSchedulesUseCase = loadProfilesWithSchedulesUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenController.kt new file mode 100644 index 00000000..d2795001 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenController.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.usecase.LoadProfilesWithSchedulesUseCase + +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.kodein.di.compose.rememberInstance + +class MedicationNotificationSuccessScreenController( + private val loadProfilesWithSchedulesUseCase: LoadProfilesWithSchedulesUseCase, + private val now: Instant = Clock.System.now() + +) : Controller() { + private val _profilesWithSchedules: + MutableStateFlow>> = + MutableStateFlow(UiState.Loading()) + val profilesWithSchedules: StateFlow>> = + _profilesWithSchedules + + init { + controllerScope.launch { + runCatching { + loadProfilesWithSchedulesUseCase(now.toLocalDateTime(TimeZone.currentSystemDefault())) + }.fold( + onSuccess = { profileWithSchedules -> + if (profileWithSchedules.first().isEmpty()) { + _profilesWithSchedules.value = UiState.Empty() + } else { + _profilesWithSchedules.value = UiState.Data(profileWithSchedules.first()) + } + }, + onFailure = { + _profilesWithSchedules.value = UiState.Error(it) + } + ) + } + } +} + +@Composable +fun rememberMedicationNotificationSuccessScreenController(): MedicationNotificationSuccessScreenController { + val loadProfilesWithSchedulesUseCase by + rememberInstance() + return remember { + MedicationNotificationSuccessScreenController( + loadProfilesWithSchedulesUseCase = loadProfilesWithSchedulesUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenController.kt new file mode 100644 index 00000000..be7bd635 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenController.kt @@ -0,0 +1,334 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.app.ActivityCompat +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.medicationplan.model.DateEvent +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.model.getCalculatedEndDate +import de.gematik.ti.erp.app.medicationplan.model.pieceableForm +import de.gematik.ti.erp.app.medicationplan.model.toMedicationSchedule +import de.gematik.ti.erp.app.medicationplan.usecase.GetDosageInstructionByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.LoadMedicationScheduleByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.PlanMedicationScheduleUseCase +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.utils.atCurrentTime +import de.gematik.ti.erp.app.utils.isMaxDate +import de.gematik.ti.erp.app.utils.maxLocalDate +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.kodein.di.compose.rememberInstance +import java.util.UUID + +@Immutable +data class PrescriptionSchedule( + val prescription: PrescriptionData.Prescription, + val medicationSchedule: MedicationSchedule, + val dosageInstruction: MedicationPlanDosageInstruction +) { + fun isScheduledEndless() = medicationSchedule.end.isMaxDate() + fun isPieceableAndStructured() = + dosageInstruction is MedicationPlanDosageInstruction.Structured && + pieceableForm.contains((prescription as PrescriptionData.Synced).medicationRequest.medication?.form) +} + +@Stable +open class MedicationPlanScheduleScreenController( + private val getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase, + private val loadMedicationScheduleByTaskIdUseCase: LoadMedicationScheduleByTaskIdUseCase, + private val getDosageInstructionByTaskIdUseCase: GetDosageInstructionByTaskIdUseCase, + private val planMedicationScheduleUseCase: PlanMedicationScheduleUseCase, + private val taskId: String, + private val now: Instant = Clock.System.now() +) : Controller() { + private val _prescriptionSchedule: + MutableStateFlow> = + MutableStateFlow(UiState.Loading()) + val prescriptionSchedule: StateFlow> = + _prescriptionSchedule + + init { + controllerScope.launch { + runCatching { + getPrescriptionByTaskIdUseCase(taskId).first() + }.fold( + onSuccess = { prescription -> + loadMedicationScheduleByTaskIdUseCase(taskId).collect { medicationSchedule -> + _prescriptionSchedule.value = UiState.Data( + PrescriptionSchedule( + prescription = prescription, + medicationSchedule = medicationSchedule ?: prescription.toMedicationSchedule(now), + dosageInstruction = getDosageInstructionByTaskIdUseCase(prescription.taskId).first() + ) + ) + } + }, + onFailure = { + _prescriptionSchedule.value = UiState.Error(it) + } + ) + } + } + + internal fun addNewTimeSlot( + uuid: String = UUID.randomUUID().toString(), + dosage: MedicationDosage, + time: LocalTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time + ) { + withMedicationSchedule { schedule -> + val formattedTime = LocalTime(time.hour, time.minute) + planMedicationScheduleUseCase( + schedule.copy( + notifications = schedule.notifications + MedicationNotification( + dosage = dosage, + time = formattedTime, + id = uuid + ) + ) + ) + } + } + + internal fun removeNotification(notification: MedicationNotification) { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase( + schedule.copy( + notifications = schedule.notifications.filter { + it.id != notification.id + } + ) + ) + } + } + + internal fun modifyNotificationTime(notification: MedicationNotification, time: LocalTime) { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase( + schedule.copy( + notifications = schedule.notifications.map { + if (it.id == notification.id) { + it.copy(time = time) + } else { + it + } + } + ) + ) + } + } + + internal fun modifyDosage(notification: MedicationNotification, dosage: MedicationDosage) { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase( + schedule.copy( + notifications = schedule.notifications.map { + if (it.id == notification.id) { + it.copy(dosage = dosage) + } else { + it + } + } + ) + ) + } + } + + internal fun activateSchedule() { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase(schedule.copy(isActive = true)) + } + } + + internal fun deactivateSchedule() { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase(schedule.copy(isActive = false)) + } + } + + internal fun changeScheduledDate(dateEvent: DateEvent) { + withMedicationSchedule { schedule -> + when (dateEvent) { + is DateEvent.StartDate -> { + planMedicationScheduleUseCase( + schedule.copy( + start = dateEvent.date + ) + ) + } + + is DateEvent.EndDate -> { + planMedicationScheduleUseCase( + schedule.copy( + end = dateEvent.date + ) + ) + } + } + } + } + + internal fun saveEndlessDateRange( + startDate: LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + ) { + withMedicationSchedule { schedule -> + planMedicationScheduleUseCase( + schedule.copy( + start = startDate, + end = maxLocalDate() + ) + ) + } + } + + internal fun calculateIndividualDateRange(now: Instant = Clock.System.now()) { + withPrescriptionSchedule { prescriptionSchedule -> + planMedicationScheduleUseCase( + prescriptionSchedule.medicationSchedule.copy( + start = prescriptionSchedule.medicationSchedule.start, + end = getCalculatedEndDate( + start = prescriptionSchedule.medicationSchedule.start.atCurrentTime(now), + amount = prescriptionSchedule.medicationSchedule.amount ?: Ratio( + numerator = Quantity( + value = "1", + unit = "" + ), + denominator = Quantity( + value = "1", + unit = "" + ) + ), + dosageInstruction = prescriptionSchedule.dosageInstruction, + form = when (prescriptionSchedule.prescription) { + is PrescriptionData.Scanned -> "" + is PrescriptionData.Synced -> + prescriptionSchedule.prescription.medicationRequest.medication?.form ?: "" + } + ) + ) + ) + } + } + + private fun withMedicationSchedule(block: suspend (MedicationSchedule) -> Unit) { + controllerScope.launch { + prescriptionSchedule.value.data?.medicationSchedule?.let { schedule -> + block(schedule) + } + } + } + + private fun withPrescriptionSchedule(block: suspend (PrescriptionSchedule) -> Unit) { + controllerScope.launch { + prescriptionSchedule.value.data?.let { prescriptionSchedule -> + block(prescriptionSchedule) + } + } + } +} + +@Composable +fun rememberMedicationPlanScheduleScreenController( + taskId: String +): MedicationPlanScheduleScreenController { + val getPrescriptionByTaskIdUseCase by rememberInstance() + val loadMedicationScheduleByTaskIdUseCase by rememberInstance() + val getDosageInstructionByTaskIdUseCase by rememberInstance() + val planMedicationScheduleUseCase by rememberInstance() + return remember { + MedicationPlanScheduleScreenController( + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase, + getDosageInstructionByTaskIdUseCase = getDosageInstructionByTaskIdUseCase, + loadMedicationScheduleByTaskIdUseCase = loadMedicationScheduleByTaskIdUseCase, + planMedicationScheduleUseCase = planMedicationScheduleUseCase, + taskId = taskId + ) + } +} + +fun Context.checkNotificationPermission( + onGranted: () -> Unit, + onDenied: () -> Unit +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + onDenied() + } else { + onGranted() + } +} + +@Composable +fun isIgnoringBatteryOptimizations(): State { + val currentState by LocalLifecycleOwner.current.lifecycle.currentStateFlow.collectAsState() + val context = LocalContext.current + return remember(currentState) { derivedStateOf { context.isIgnoringBatteryOptimizations() } } +} + +fun Context.isIgnoringBatteryOptimizations(): Boolean { + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(packageName) +} + +@SuppressLint("BatteryLife") +fun Context.requestIgnoreBatteryOptimizations() { + val intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:$packageName")).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + startActivity(intent) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/scheduler/DateTimeChangeReceiver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/scheduler/DateTimeChangeReceiver.kt new file mode 100644 index 00000000..22a53559 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/scheduler/DateTimeChangeReceiver.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.scheduler + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import de.gematik.ti.erp.app.medicationplan.worker.scheduleReminderWorker +import kotlin.time.Duration + +class DateTimeChangeReceiver : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent + ) { + when (intent.action) { + Intent.ACTION_DATE_CHANGED, + Intent.ACTION_TIME_CHANGED, + Intent.ACTION_TIMEZONE_CHANGED + -> + context.scheduleReminderWorker(Duration.ZERO) + else -> {} + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreen.kt new file mode 100644 index 00000000..f476466f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreen.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes +import de.gematik.ti.erp.app.medicationplan.presentation.rememberMedicationListScheduleScreenController +import de.gematik.ti.erp.app.medicationplan.ui.components.ProfileHeader +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationListScheduleScreenPreview +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationListScheduleScreenPreviewParameter +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.EmptyScreenComponent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState + +class MedicationListScheduleScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + val controller = rememberMedicationListScheduleScreenController() + val profilesWithSchedulesData by controller.profilesWithSchedules.collectAsStateWithLifecycle() + BackHandler { + navController.navigateUp() + } + MedicationListScheduleScreenScaffold( + profilesWithSchedules = profilesWithSchedulesData, + listState = listState, + onClickSchedule = { taskId -> + navController.navigate(MedicationPlanRoutes.MedicationPlanPerPrescription.path(taskId)) + }, + onBack = { + navController.navigateUp() + } + ) + } +} + +@Composable +private fun MedicationListScheduleScreenScaffold( + profilesWithSchedules: UiState>, + listState: LazyListState, + onClickSchedule: (String) -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier, + topBarTitle = stringResource(R.string.medication_plan_title), + listState = listState, + onBack = onBack, + navigationMode = NavigationBarMode.Back, + content = { contentPadding -> + + UiStateMachine( + state = profilesWithSchedules, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + EmptyScreenComponent( + title = stringResource(R.string.empty_medication_plan_title), + body = stringResource(R.string.empty_medication_plan_info), + button = {} + ) + }, + onError = { + ErrorScreenComponent() + }, + onContent = { profilesWithSchedules -> + + MedicationListScheduleScreenContent( + listState = listState, + contentPadding = contentPadding, + profilesWithSchedules = profilesWithSchedules, + onClickSchedule = onClickSchedule + ) + } + ) + } + ) +} + +@Composable +private fun MedicationListScheduleScreenContent( + listState: LazyListState, + contentPadding: PaddingValues, + profilesWithSchedules: List, + onClickSchedule: (String) -> Unit +) { + LazyColumn( + contentPadding = contentPadding, + state = listState + ) { + items( + items = profilesWithSchedules + ) { profileWithSchedules -> + SpacerMedium() + ProfileHeader(profileWithSchedules.profile) + SpacerMedium() + profileWithSchedules.medicationSchedules.forEach { schedule -> + ScheduleLineItem(schedule, onClickSchedule) + } + SpacerMedium() + } + } +} + +@Composable +private fun ScheduleLineItem(schedule: MedicationSchedule, onClickSchedule: (String) -> Unit) { + val info = when { + schedule.isActive -> stringResource(R.string.medication_plan_active) + else -> stringResource(R.string.medication_plan_inactive) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { onClickSchedule(schedule.taskId) }, + role = Role.Button + ) + .padding( + PaddingDefaults.Medium + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(1f), + text = schedule.message.title, + style = AppTheme.typography.body1, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerMedium() + Text( + text = info, + style = AppTheme.typography.body1 + ) + SpacerMedium() + Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@LightDarkPreview +@Composable +fun MedicationListScheduleScreenPreview( + @PreviewParameter( + MedicationListScheduleScreenPreviewParameter::class + ) previewData: MedicationListScheduleScreenPreview +) { + PreviewAppTheme { + val listState = rememberLazyListState() + + MedicationListScheduleScreenScaffold( + listState = listState, + profilesWithSchedules = previewData.state, + onClickSchedule = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreen.kt new file mode 100644 index 00000000..8ddceb92 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreen.kt @@ -0,0 +1,288 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.components + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowForward +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.material.icons.sharp.Alarm +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.components.getDayTimeImageAndDescription +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes +import de.gematik.ti.erp.app.medicationplan.presentation.rememberMedicationNotificationSuccessScreenController +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationSuccessScreenPreview +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationSuccessScreenPreviewParameter +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.profiles.ui.components.Avatar +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.EmptyScreenComponent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState + +private const val NOT_AVAILABLE = -1 + +class MedicationNotificationSuccessScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + val baseActivity = LocalActivity.current as BaseActivity + val controller = rememberMedicationNotificationSuccessScreenController() + val profilesWithSchedulesState by controller.profilesWithSchedules.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + BackHandler { + navController.popBackStack() + } + + MedicationNotificationSuccessScreenScaffold( + listState = listState, + profilesWithSchedulesState = profilesWithSchedulesState, + onClickMedicationPlan = { navController.navigate(MedicationPlanRoutes.MedicationPlanList.path()) }, + onBack = { + baseActivity.medicationSuccessHasBeenShown() + navController.navigate(PrescriptionRoutes.PrescriptionsScreen.path()) + } + ) + } +} + +@Composable +fun MedicationNotificationSuccessScreenScaffold( + listState: LazyListState, + profilesWithSchedulesState: UiState>, + onClickMedicationPlan: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier, + topBarTitle = stringResource(R.string.medication_plan_title), + listState = listState, + onBack = onBack, + navigationMode = NavigationBarMode.Close, + bottomBar = { + MedicationNotificationSuccessScreenBottomBar( + onClickMedicationPlan = onClickMedicationPlan + ) + }, + content = { contentPadding -> + + UiStateMachine( + state = profilesWithSchedulesState, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + EmptyScreenComponent( + title = stringResource(R.string.medication_notification_empty_title), + body = stringResource(R.string.medication_notification_empty_info), + image = { EmptyScreenImage() } + ) {} + }, + onError = { + ErrorScreenComponent() + }, + onContent = { profilesWithSchedules -> + MedicationNotificationSuccessScreenContent( + profilesWithSchedules = profilesWithSchedules, + listState = listState, + contentPadding = contentPadding + ) + } + ) + } + ) +} + +@Composable +fun MedicationNotificationSuccessScreenBottomBar(onClickMedicationPlan: () -> Unit) { + BottomAppBar( + backgroundColor = MaterialTheme.colors.surface + ) { + val color = AppTheme.colors.primary600 + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + role = Role.Button + ) { onClickMedicationPlan() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(R.string.medication_plan_reminder_bottom_text), color = color) + SpacerSmall() + Icon(Icons.AutoMirrored.Rounded.ArrowForward, contentDescription = null, tint = color) + } + } +} + +@Composable +fun EmptyScreenImage() { + Icon( + imageVector = Icons.Sharp.Alarm, + contentDescription = null, + tint = AppTheme.colors.primary600, + modifier = Modifier + .size(SizeDefaults.sevenfold) + ) +} + +@Composable +fun MedicationNotificationSuccessScreenContent( + listState: LazyListState, + contentPadding: PaddingValues, + profilesWithSchedules: List +) { + LazyColumn( + contentPadding = contentPadding, + state = listState + ) { + item { SpacerLarge() } + items( + items = profilesWithSchedules + ) { profileWithSchedules -> + if (profilesWithSchedules.size > 1) { + ProfileHeader(profileWithSchedules.profile) + SpacerMedium() + } + profileWithSchedules.medicationSchedules.forEach { schedule -> + val title = schedule.message.title + ScheduleTitle(title) + SpacerSmall() + schedule.notifications.forEach { notification -> + val (image, description) = getDayTimeImageAndDescription(notification) + MedicationPlanReminderCard( + imageResource = image, + title = when (description) { + NOT_AVAILABLE -> "" + else -> stringResource(id = description) + }, + description = when { + notification.dosage.form.isNotBlank() -> stringResource( + R.string.medication_plan_success_dosage_success, + requireNotNull(notification.time), + requireNotNull(notification.dosage.form) + ) + + else -> stringResource(R.string.medication_plan_success_dosage) + } + ) + SpacerMedium() + } + SpacerMedium() + } + } + } +} + +@Composable +fun ScheduleTitle(title: String) { + Text( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + text = title, + style = AppTheme.typography.subtitle1 + ) +} + +@Composable +fun ProfileHeader(profile: ProfilesUseCaseData.Profile) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + modifier = Modifier.size(SizeDefaults.sixfold), + emptyIcon = Icons.Rounded.PersonOutline, + profile = profile, + iconModifier = Modifier.size(SizeDefaults.doubleHalf) + ) + SpacerMedium() + Text( + modifier = Modifier.weight(1f), + text = profile.name, + style = AppTheme.typography.subtitle2 + ) + } +} + +@LightDarkPreview +@Composable +fun MedicationNotificationSuccessScreenPreview( + @PreviewParameter(MedicationSuccessScreenPreviewParameter::class) previewData: MedicationSuccessScreenPreview +) { + PreviewAppTheme { + val listState = rememberLazyListState() + + MedicationNotificationSuccessScreenScaffold( + listState = listState, + profilesWithSchedulesState = previewData.state, + onClickMedicationPlan = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreen.kt new file mode 100644 index 00000000..faa2253e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreen.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + *j + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationPlanDosageInfoPreview +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationPlanDosageInfoPreviewParameter +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.presentation.rememberDosageInstructionBottomSheetScreenController +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.Body2lText +import de.gematik.ti.erp.app.utils.compose.fullscreen.FullScreenLoadingIndicator +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.Subtitle1Text + +class MedicationPlanDosageInfoBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = true) { + @Composable + override fun Content() { + val taskId = navBackStackEntry.arguments?.getString("taskId") + val controller = rememberDosageInstructionBottomSheetScreenController(taskId ?: "") + val dosageInstructionState by controller.dosageInstruction.collectAsStateWithLifecycle() + + UiStateMachine( + state = dosageInstructionState, + onError = { _ -> + ErrorScreenComponent() + }, + onLoading = { + FullScreenLoadingIndicator() + }, + onContent = { dosageInstruction -> + MedicationPlanDosageInfoContent(dosageInstruction = dosageInstruction) + }, + onEmpty = { + ErrorScreenComponent() + } + ) + } +} + +@Composable +private fun MedicationPlanDosageInfoContent(dosageInstruction: MedicationPlanDosageInstruction) { + when (dosageInstruction) { + is MedicationPlanDosageInstruction.External -> ExternalInfo() + MedicationPlanDosageInstruction.Empty -> EmptyInfo() + is MedicationPlanDosageInstruction.FreeText -> FreeTextInfo(dosageInstruction) + is MedicationPlanDosageInstruction.Structured -> StructuredInfo(dosageInstruction) + } +} + +@Composable +private fun StructuredInfo(dosageInstruction: MedicationPlanDosageInstruction.Structured) { + InfoContent( + dosageText = dosageInstruction.text, + body = stringResource(R.string.structured_dosage_info_body) + ) { + dosageInstruction.interpretation.forEach { (dayTime, times) -> + when (dayTime) { + MedicationPlanDosageInstruction.DayTime.MORNING -> Body2lText(stringResource(R.string.structured_dosage_morning, times)) + MedicationPlanDosageInstruction.DayTime.NOON -> Body2lText(stringResource(R.string.structured_dosage_noon, times)) + MedicationPlanDosageInstruction.DayTime.EVENING -> Body2lText(stringResource(R.string.structured_dosage_evening, times)) + MedicationPlanDosageInstruction.DayTime.NIGHT -> Body2lText(stringResource(R.string.structured_dosage_night, times)) + } + } + } +} + +@Composable +private fun FreeTextInfo(dosageInstruction: MedicationPlanDosageInstruction.FreeText) { + InfoContent( + dosageText = dosageInstruction.text, + body = stringResource(R.string.freetext_dosage_info_body) + ) +} + +@Composable +private fun EmptyInfo() { + InfoContent( + dosageText = stringResource(R.string.empty_dosage_info_dosage_text), + body = stringResource(R.string.empty_dosage_info_body) + ) +} + +@Composable +private fun ExternalInfo() { + InfoContent( + dosageText = stringResource(R.string.external_dosage_info_dosage_text), + body = stringResource(R.string.external_dosage_info_body) + ) +} + +@Composable +private fun InfoContent(dosageText: String, body: String, footer: (@Composable () -> Unit)? = null) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + .padding(top = PaddingDefaults.Medium, bottom = PaddingDefaults.XXLarge), + verticalArrangement = Arrangement.spacedBy(SizeDefaults.one) + ) { + Subtitle1Text(stringResource(R.string.dosage_info_bottomsheet_header)) + Body2lText(dosageText) + Body2lText(body) + footer?.invoke() + } +} + +@LightDarkPreview +@Composable +fun MedicationPlanDosageInfoContentPreview( + @PreviewParameter(MedicationPlanDosageInfoPreviewParameter::class) previewData: MedicationPlanDosageInfoPreview +) { + PreviewAppTheme { + MedicationPlanDosageInfoContent(previewData.dosageInstruction) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreen.kt new file mode 100644 index 00000000..b561608b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreen.kt @@ -0,0 +1,749 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import android.Manifest +import android.app.TimePickerDialog +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.filled.DoDisturbOn +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.sharp.Alarm +import androidx.compose.material.IconButton +//noinspection UsingMaterialAndMaterial3Libraries +import androidx.compose.material.TextButton +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material.icons.rounded.RadioButtonUnchecked +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationPlanScheduleScreenPreviewParameter +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationScheduleScreenPreview +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes +import de.gematik.ti.erp.app.medicationplan.presentation.PrescriptionSchedule +import de.gematik.ti.erp.app.medicationplan.presentation.checkNotificationPermission +import de.gematik.ti.erp.app.medicationplan.presentation.isIgnoringBatteryOptimizations +import de.gematik.ti.erp.app.medicationplan.presentation.rememberMedicationPlanScheduleScreenController +import de.gematik.ti.erp.app.medicationplan.presentation.requestIgnoreBatteryOptimizations +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.InputField +import de.gematik.ti.erp.app.utils.compose.LabeledSwitch +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.OutlinedElevatedCard +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.formattedStringShort +import de.gematik.ti.erp.app.utils.isBeforeCurrentDate +import de.gematik.ti.erp.app.utils.isInFuture +import de.gematik.ti.erp.app.utils.isMaxDate +import de.gematik.ti.erp.app.utils.toHourMinuteString +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +class MedicationPlanScheduleScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val taskId = + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID + ) ?: return + val context = LocalContext.current + val dialogScaffold = LocalDialog.current + val controller = rememberMedicationPlanScheduleScreenController(taskId) + val prescriptionScheduleData by controller.prescriptionSchedule.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + val timePickerEvent = ComposableEvent() + val showNotificationPermissionDialogEvent = ComposableEvent() + val showChangeDosageDialogEvent = ComposableEvent() + val isIgnoringBatteryOptimizations by isIgnoringBatteryOptimizations() + val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val defaultMedicationDosage = prescriptionScheduleData.data?.medicationSchedule?.notifications?.lastOrNull()?.dosage + ?: MedicationDosage( + stringResource(R.string.medication_plan_default_form), + context.getString( + R.string.medication_plan_default_dosage + ) + ) + BackHandler { + navController.popBackStack() + } + + ChangeMedicationDosageDialog( + event = showChangeDosageDialogEvent, + onDosageChanged = { notification, dosage -> + controller.modifyDosage(notification, dosage) + } + ) + + timePickerEvent.listen { notification -> + TimePickerDialog( + context, + { _, hour, minute -> + controller.modifyNotificationTime( + notification = notification, + time = LocalTime(hour, minute) + ) + }, + notification.time.hour, + notification.time.minute, + true + ).show() + } + + @Requirement( + "O.Plat_5#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Reminder notifications are deactivated by default.", + codeLines = 50 + ) + val notificationPermissionLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { + if (!it) { + showNotificationPermissionDialogEvent.trigger(Unit) + controller.deactivateSchedule() + } + } + + val showIgnoreBatteryOptimizationDialog = ComposableEvent() + + IgnoreBatteryOptimizationDialog( + showDisableBatteryOptimizationDialog = showIgnoreBatteryOptimizationDialog, + dialogScaffold = dialogScaffold + ) { + context.requestIgnoreBatteryOptimizations() + controller.activateSchedule() + } + + MedicationPlanScheduleScaffold( + listState = listState, + prescriptionScheduleData = prescriptionScheduleData, + currentDate = currentDate, + isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations, + onAddNewTimeSlot = { + controller.addNewTimeSlot( + dosage = defaultMedicationDosage + ) + }, + onRemoveNotificationTime = { notification -> + controller.removeNotification(notification) + }, + onClickChangeDateRange = { + navController.navigate( + MedicationPlanRoutes.ScheduleDateRange.path(taskId = taskId) + ) + }, + onActivateSchedule = { + controller.activateSchedule() + @Requirement( + "O.Plat_5#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for notification permission and open launcher if not granted.", + codeLines = 50 + ) + context.checkNotificationPermission( + onDenied = { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + }, + onGranted = { + } + ) + controller.activateSchedule() + }, + onDeactivateSchedule = { + controller.deactivateSchedule() + }, + onNotificationTimeClick = { notification -> + timePickerEvent.trigger( + notification + ) + }, + onClickDosageInfo = { + taskId.let { + navController.navigate( + MedicationPlanRoutes.MedicationPlanDosageInfo.path(taskId = taskId) + ) + } + }, + onDosageClicked = { notification -> + showChangeDosageDialogEvent.trigger(notification) + }, + onShowBatteryOptimizationDialog = { showIgnoreBatteryOptimizationDialog.trigger(Unit) }, + onBack = { navController.popBackStack() } + ) + } +} + +@Composable +fun MedicationPlanScheduleScaffold( + listState: LazyListState, + prescriptionScheduleData: UiState, + currentDate: LocalDate, + isIgnoringBatteryOptimizations: Boolean, + onAddNewTimeSlot: () -> Unit, + onRemoveNotificationTime: (MedicationNotification) -> Unit, + onNotificationTimeClick: (MedicationNotification) -> Unit, + onClickChangeDateRange: () -> Unit, + onClickDosageInfo: () -> Unit, + onDosageClicked: (MedicationNotification) -> Unit, + onActivateSchedule: () -> Unit, + onDeactivateSchedule: () -> Unit, + onShowBatteryOptimizationDialog: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + listState = listState, + topBarTitle = stringResource(R.string.medication_plan_title), + navigationMode = NavigationBarMode.Back, + onBack = onBack + + ) { contentPadding -> + + UiStateMachine( + state = prescriptionScheduleData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { prescriptionSchedule -> + + MedicationScheduleScreenContent( + listState = listState, + contentPadding = contentPadding, + currentDate = currentDate, + prescriptionSchedule = prescriptionSchedule, + isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations, + onAddNewItem = onAddNewTimeSlot, + onRemoveNotificationTime = onRemoveNotificationTime, + onClickChangeDateRange = onClickChangeDateRange, + onActivateSchedule = onActivateSchedule, + onDeactivateSchedule = onDeactivateSchedule, + onNotificationTimeClick = onNotificationTimeClick, + onClickDosageInfo = onClickDosageInfo, + onDosageClicked = onDosageClicked, + onShowBatteryOptimizationDialog = onShowBatteryOptimizationDialog + ) + } + ) + } +} + +@Composable +fun ChangeMedicationDosageDialog( + event: ComposableEvent, + onDosageChanged: (MedicationNotification, MedicationDosage) -> Unit +) { + val dialog = LocalDialog.current + var dosage by remember { + mutableStateOf(event.payload?.dosage ?: MedicationDosage("", "")) + } + event.listen { + dosage = it.dosage + dialog.show { d -> + ErezeptAlertDialog( + title = stringResource(R.string.adjust_dosage_dialog_title), + body = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + InputField( + modifier = Modifier, + value = dosage.ratio, + onValueChange = { value -> + dosage = dosage.copy(ratio = value) + }, + label = { + Text(stringResource(R.string.adjust_dosage_amount_label)) + }, + keyBoardType = KeyboardType.Number, + onSubmit = {} + ) + SpacerMedium() + InputField( + modifier = Modifier, + value = dosage.form, + onValueChange = { dosage = dosage.copy(form = it) }, + label = { Text(stringResource(R.string.adjust_dosage_form_label)) }, + keyBoardType = KeyboardType.Text, + onSubmit = {} + ) + } + }, + onConfirmRequest = { + onDosageChanged(it, dosage) + d.dismiss() + }, + onDismissRequest = { + d.dismiss() + } + ) + } + } +} + +@Suppress("LongMethod", "MaxLineLength") +@Composable +private fun MedicationScheduleScreenContent( + listState: LazyListState, + contentPadding: PaddingValues, + prescriptionSchedule: PrescriptionSchedule, + currentDate: LocalDate, + isIgnoringBatteryOptimizations: Boolean, + onAddNewItem: () -> Unit, + onRemoveNotificationTime: (MedicationNotification) -> Unit, + onNotificationTimeClick: (MedicationNotification) -> Unit, + onClickChangeDateRange: () -> Unit, + onClickDosageInfo: () -> Unit, + onDosageClicked: (MedicationNotification) -> Unit, + onActivateSchedule: () -> Unit, + onDeactivateSchedule: () -> Unit, + onShowBatteryOptimizationDialog: () -> Unit +) { + LazyColumn( + contentPadding = contentPadding, + state = listState, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) { + val name = when (prescriptionSchedule.prescription) { + is PrescriptionData.Scanned -> prescriptionSchedule.prescription.name + is PrescriptionData.Synced -> prescriptionSchedule.prescription.name + } + + item { + SpacerMedium() + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Sharp.Alarm, + contentDescription = null, + tint = AppTheme.colors.primary600, + modifier = Modifier + .size(SizeDefaults.sevenfold) + ) + SpacerMedium() + Text( + modifier = Modifier + .fillMaxWidth(), + text = if (name.isNullOrBlank()) { + stringResource(R.string.medication_plan_missing_medication_name) + } else { + name + }, + style = AppTheme.typography.h6, + textAlign = TextAlign.Center + ) + } + } + + item { + SpacerMedium() + ScheduleSettingsAndDosageCard( + schedule = prescriptionSchedule, + isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations, + onIgnoreBatteryOptimizations = onShowBatteryOptimizationDialog, + onActivateSchedule = onActivateSchedule, + onDeactivateSchedule = onDeactivateSchedule, + onClickDosageInfo = onClickDosageInfo + ) + } + + if (prescriptionSchedule.medicationSchedule.isActive) { + item { + SpacerXXLarge() + Text(stringResource(R.string.plan_notification_times_header), style = AppTheme.typography.h6) + } + + item { + val shape = RoundedCornerShape(SizeDefaults.doubleHalf) + SpacerMedium() + OutlinedElevatedCard { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .clickable { + onClickChangeDateRange() + } + .padding(PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val text = getScheduleDurationString(prescriptionSchedule, currentDate) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(end = PaddingDefaults.Small) + ) { + Text(stringResource(R.string.medication_schedule_repeat), style = AppTheme.typography.body1) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = text, + style = AppTheme.typography.body1l, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + Icon(Icons.AutoMirrored.Outlined.ArrowRight, contentDescription = null, tint = AppTheme.colors.neutral600) + } + } + } + item { + SpacerMedium() + } + + item { + OutlinedElevatedCard { + Column( + modifier = Modifier + .padding( + vertical = PaddingDefaults.Medium + ) + .padding(end = PaddingDefaults.Medium), + verticalArrangement = Arrangement.spacedBy( + SizeDefaults.one + ) + ) { + prescriptionSchedule.medicationSchedule.notifications.forEach { notification -> + + Row( + horizontalArrangement = Arrangement.spacedBy(SizeDefaults.half), + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.padding(horizontal = PaddingDefaults.Tiny), + onClick = { + onRemoveNotificationTime(notification) + } + ) { + Icon( + imageVector = Icons.Filled.DoDisturbOn, + contentDescription = null, + tint = AppTheme.colors.red600 + ) + } + Text( + text = notification.time.toHourMinuteString(), + modifier = Modifier + .background( + shape = RoundedCornerShape(SizeDefaults.one), + color = AppTheme.colors.neutral200 + ) + .clip(RoundedCornerShape(SizeDefaults.one)) + .clickable { + onNotificationTimeClick(notification) + } + .padding( + horizontal = PaddingDefaults.Tiny, + vertical = SizeDefaults.threeQuarter + ), + style = AppTheme.typography.body1 + ) + Spacer(modifier = Modifier.weight(1f)) + TextButton( + onClick = { + onDosageClicked(notification) + } + ) { + Text("${notification.dosage.ratio} ${notification.dosage.form}") + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onAddNewItem() }, + verticalAlignment = Alignment.CenterVertically + ) { + SpacerMedium() + Icon( + imageVector = Icons.Filled.AddCircle, + contentDescription = null, + tint = AppTheme.colors.green600 + ) + SpacerMedium() + Text( + text = stringResource(R.string.medication_schedule_add_notification_time), + style = AppTheme.typography.body1, + color = AppTheme.colors.primary600 + ) + } + } + } + } + } + } +} + +@Composable +private fun getScheduleDurationString( + prescriptionSchedule: PrescriptionSchedule, + currentDate: LocalDate +): String { + val text = when { + prescriptionSchedule.medicationSchedule.end.isMaxDate() -> stringResource(R.string.medication_plan_endless) + prescriptionSchedule.medicationSchedule.end.isBeforeCurrentDate(currentDate) -> + stringResource(R.string.medication_plan_ended) + prescriptionSchedule.medicationSchedule.start.isInFuture(currentDate) -> stringResource( + R.string.medicationPlanDuration, + prescriptionSchedule.medicationSchedule.start.formattedStringShort(), + prescriptionSchedule.medicationSchedule.end.formattedStringShort() + ) + else -> stringResource( + R.string.medication_plan_ends, + prescriptionSchedule.medicationSchedule.end.formattedStringShort() + ) + } + return text +} + +@Composable +fun IgnoreBatteryOptimizationDialog( + showDisableBatteryOptimizationDialog: ComposableEvent, + dialogScaffold: DialogScaffold, + onIgnoreBatteryOptimizations: () -> Unit +) { + showDisableBatteryOptimizationDialog.listen { + dialogScaffold.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.ignore_battery_optimization_dialog_title), + bodyText = stringResource(R.string.ignore_battery_optimization_dialog_info), + dismissText = stringResource(R.string.ignore_battery_optimization_dialog_dissmiss), + confirmText = stringResource(R.string.ignore_battery_optimization_dialog_confirm), + onConfirmRequest = { onIgnoreBatteryOptimizations() }, + onDismissRequest = { dialog.dismiss() } + ) + } + } +} + +@Composable +fun ScheduleSettingsAndDosageCard( + schedule: PrescriptionSchedule, + isIgnoringBatteryOptimizations: Boolean, + onIgnoreBatteryOptimizations: () -> Unit, + onActivateSchedule: () -> Unit, + onDeactivateSchedule: () -> Unit, + onClickDosageInfo: () -> Unit +) { + OutlinedElevatedCard { + ScheduleActivitySection(schedule, onActivateSchedule, onDeactivateSchedule) + if (!isIgnoringBatteryOptimizations) { + IgnoreBatteryOptimizationSection(onIgnoreBatteryOptimizations) + } + DosageInfo(schedule, onClickDosageInfo) + } +} + +@Composable +fun IgnoreBatteryOptimizationSection(onIgnoreBatteryOptimizations: () -> Unit) { + Column( + modifier = Modifier + .clickable { + onIgnoreBatteryOptimizations() + } + .fillMaxWidth() + .padding(PaddingDefaults.Medium) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + style = AppTheme.typography.body1, + text = stringResource(R.string.battery_optimazation_info) + ) + Icon( + Icons.Rounded.RadioButtonUnchecked, + null, + tint = AppTheme.colors.neutral400 + ) + } + Text( + stringResource(R.string.battery_optimization_info_label), + style = AppTheme.typography.body2l, + color = AppTheme.colors.primary600 + ) + } +} + +@Composable +fun DosageInfo(schedule: PrescriptionSchedule, onClickDosageInfo: () -> Unit) { + Column( + modifier = Modifier + .clickable { + onClickDosageInfo() + } + .fillMaxWidth() + .padding(PaddingDefaults.Medium) + ) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + modifier = Modifier.weight(1f), + style = AppTheme.typography.body1, + text = when (schedule.dosageInstruction) { + is MedicationPlanDosageInstruction.FreeText -> schedule.dosageInstruction.text + is MedicationPlanDosageInstruction.Structured -> schedule.dosageInstruction.text + is MedicationPlanDosageInstruction.Empty -> stringResource(R.string.dosage_instruction_empty) + is MedicationPlanDosageInstruction.External -> stringResource(R.string.dosage_instruction_external) + } + ) + Icon( + modifier = Modifier, + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = AppTheme.colors.primary600 + ) + } + Text(stringResource(R.string.plan_schedule_dosage_instruction_label), style = AppTheme.typography.body2l) + } +} + +@Composable +private fun ScheduleActivitySection( + schedule: PrescriptionSchedule, + onActivateSchedule: () -> Unit, + onDeactivateSchedule: () -> Unit +) { + LabeledSwitch( + checked = schedule.medicationSchedule.isActive, + onCheckedChange = { checked -> + if (checked) { + onActivateSchedule() + } else { + onDeactivateSchedule() + } + } + ) { + Row( + modifier = Modifier.weight(1.0f) + ) { + Column( + modifier = Modifier + .weight(1.0f) + ) { + Text( + text = stringResource(R.string.activate_schedule_switch_text), + style = AppTheme.typography.body1 + ) + } + } + } +} + +@LightDarkPreview +@Composable +fun MedicationPlanScheduleScreenPreview( + @PreviewParameter(MedicationPlanScheduleScreenPreviewParameter::class) previewData: MedicationScheduleScreenPreview +) { + PreviewAppTheme { + val listState = rememberLazyListState() + + MedicationPlanScheduleScaffold( + listState = listState, + prescriptionScheduleData = previewData.state, + currentDate = previewData.currentDate, + isIgnoringBatteryOptimizations = previewData.isIgnoringBatteryOptimizations, + onDosageClicked = {}, + onClickDosageInfo = {}, + onActivateSchedule = {}, + onDeactivateSchedule = {}, + onAddNewTimeSlot = {}, + onClickChangeDateRange = {}, + onRemoveNotificationTime = {}, + onNotificationTimeClick = {}, + onShowBatteryOptimizationDialog = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreen.kt new file mode 100644 index 00000000..ad0e3ec9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreen.kt @@ -0,0 +1,483 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.sharp.Check +import androidx.compose.material.Checkbox +import androidx.compose.material.TextButton +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.model.DateEvent +import de.gematik.ti.erp.app.medicationplan.presentation.PrescriptionSchedule +import de.gematik.ti.erp.app.medicationplan.presentation.rememberMedicationPlanScheduleScreenController +import de.gematik.ti.erp.app.medicationplan.ui.preview.ScheduleDateRangeScreenPreview +import de.gematik.ti.erp.app.medicationplan.ui.preview.ScheduleDateRangeScreenPreviewParameter +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.HintCard +import de.gematik.ti.erp.app.utils.compose.HintCardDefaults +import de.gematik.ti.erp.app.utils.compose.HintCloseButton +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.formattedString +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate + +const val EpochMillisPerDay = 86400000L +class ScheduleDateRangeScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + val taskId = + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID + ) ?: return + + val controller = rememberMedicationPlanScheduleScreenController(taskId) + + val prescriptionScheduleData by controller.prescriptionSchedule.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + + val changeDateEvent = ComposableEvent() + val dialog = LocalDialog.current + BackHandler { + navController.popBackStack() + } + + ChangeDateDialog( + event = changeDateEvent, + isPieceableAndStructured = prescriptionScheduleData.data?.isPieceableAndStructured() ?: false, + dialog = dialog, + onDateChanged = { dateEvent -> + controller.changeScheduledDate(dateEvent) + }, + onSelectIndividualDateRange = { controller.calculateIndividualDateRange() } + ) + + ScheduleDateRangeScreenScaffold( + prescriptionScheduleState = prescriptionScheduleData, + listState = listState, + onClickEndlessDateRange = { + controller.saveEndlessDateRange() + }, + onSelectIndividualDateRange = { + controller.calculateIndividualDateRange() + }, + onChangeScheduledDate = { dateEvent -> + changeDateEvent.trigger(dateEvent) + }, + onBack = { + navController.navigateUp() + } + ) + } +} + +@Composable +fun ScheduleDateRangeScreenScaffold( + prescriptionScheduleState: UiState, + listState: LazyListState, + onClickEndlessDateRange: () -> Unit, + onSelectIndividualDateRange: () -> Unit, + onChangeScheduledDate: (DateEvent) -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + listState = listState, + topBarTitle = stringResource(R.string.medication_plan_title), + navigationMode = NavigationBarMode.Back, + onBack = onBack + ) { contentPadding -> + + UiStateMachine( + state = prescriptionScheduleState, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { prescriptionSchedule -> + ScheduleDateRangeScreenContent( + prescriptionSchedule = prescriptionSchedule, + contentPadding = contentPadding, + listState = listState, + onClickEndlessDateRange = onClickEndlessDateRange, + onSelectIndividualDateRange = onSelectIndividualDateRange, + onChangeScheduledDate = onChangeScheduledDate + ) + } + ) + } +} + +@Composable +private fun ScheduleDateRangeScreenContent( + prescriptionSchedule: PrescriptionSchedule, + contentPadding: PaddingValues, + listState: LazyListState, + onClickEndlessDateRange: () -> Unit, + onSelectIndividualDateRange: () -> Unit, + onChangeScheduledDate: (DateEvent) -> Unit + +) { + LazyColumn( + modifier = Modifier.padding(vertical = PaddingDefaults.Medium), + contentPadding = contentPadding, + state = listState + ) { + item { + DateRangeSection( + text = stringResource(R.string.schedule_date_range_unlimited), + selected = prescriptionSchedule.isScheduledEndless(), + onSelect = onClickEndlessDateRange + ) + } + item { + DateRangeSection( + text = stringResource(R.string.schedule_date_range_individually), + selected = !prescriptionSchedule.isScheduledEndless(), + onSelect = onSelectIndividualDateRange + ) + } + + if (!prescriptionSchedule.isScheduledEndless()) { + item { + ScheduleDateSection( + text = stringResource(R.string.schedule_date_range_start), + date = prescriptionSchedule.medicationSchedule.start, + onclickDate = { + onChangeScheduledDate( + DateEvent.StartDate( + date = prescriptionSchedule.medicationSchedule.start + ) + ) + } + ) + } + item { + ScheduleDateSection( + text = stringResource(R.string.schedule_date_range_end), + date = prescriptionSchedule.medicationSchedule.end, + onclickDate = { + onChangeScheduledDate( + DateEvent.EndDate( + date = prescriptionSchedule.medicationSchedule.end + ) + ) + } + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChangeDateDialog( + event: ComposableEvent, + isPieceableAndStructured: Boolean, + onDateChanged: (DateEvent) -> Unit, + onSelectIndividualDateRange: () -> Unit, + dialog: DialogScaffold +) { + event.listen { + dialog.show { dialog -> + val initialDateMillis = when (event.payload) { + is DateEvent.StartDate -> ( + event.payload as DateEvent.StartDate + ).date.toEpochDays() * EpochMillisPerDay + is DateEvent.EndDate -> ( + event.payload as DateEvent.EndDate + ) + .date.toEpochDays() * EpochMillisPerDay + null -> Clock.System.now().toEpochMilliseconds() + } + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = initialDateMillis + ) + + var showHint by remember { mutableStateOf(true) } + LazyColumn { + item { + Column { + SpacerSmall() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = PaddingDefaults.Small, end = PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + dialog.dismiss() + } + ) { + Text(text = stringResource(R.string.cancel)) + } + Spacer(modifier = Modifier.weight(1.0f)) + PrimaryButton(onClick = { + when (event.payload) { + is DateEvent.StartDate -> { + onDateChanged( + DateEvent.StartDate( + date = LocalDate.fromEpochDays( + datePickerState.selectedDateMillis?.div( + EpochMillisPerDay + )?.toInt() ?: 0 + ) + ) + ) + } + + else -> { + onDateChanged( + DateEvent.EndDate( + date = LocalDate.fromEpochDays( + datePickerState.selectedDateMillis?.div( + EpochMillisPerDay + )?.toInt() ?: 0 + ) + ) + ) + } + } + dialog.dismiss() + }) { + Text( + text = stringResource(R.string.date_picker_save_date), + color = AppTheme.colors.neutral000 + ) + } + } + } + } + item { + if (isPieceableAndStructured && event.payload is DateEvent.EndDate) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + modifier = Modifier.padding(start = PaddingDefaults.Tiny), + checked = initialDateMillis == datePickerState.selectedDateMillis, + onCheckedChange = { checked -> + if (checked) { + onSelectIndividualDateRange() + dialog.dismiss() + } + } + ) + SpacerMedium() + Text(stringResource(R.string.date_picker_to_calculated_date)) + } + if (showHint) { + HintCard( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + properties = HintCardDefaults.properties( + backgroundColor = AppTheme.colors.primary100, + contentColor = AppTheme.colors.primary900, + border = BorderStroke(1.dp, AppTheme.colors.primary600) + ), + image = { + Icon( + Icons.Outlined.Info, + null, + modifier = Modifier + .padding(it) + .requiredSize(24.dp), + tint = AppTheme.colors.primary600 + ) + }, + body = { + Text(text = stringResource(R.string.date_picker_calculateed_date_info)) + }, + title = null, + close = { + HintCloseButton(tint = AppTheme.colors.primary600, innerPadding = it) { + showHint = false + } + } + ) + } + } + } + item { + val text = when (event.payload) { + is DateEvent.StartDate -> stringResource(R.string.schedule_date_range_save_start) + else -> stringResource(R.string.schedule_date_range_save_end) + } + Box(modifier = Modifier.padding(PaddingDefaults.Medium), contentAlignment = Alignment.Center) { + DatePicker( + state = datePickerState, + colors = DatePickerDefaults.colors( + selectedDayContainerColor = AppTheme.colors.primary600, + selectedDayContentColor = AppTheme.colors.neutral000, + selectedYearContainerColor = AppTheme.colors.primary600, + selectedYearContentColor = AppTheme.colors.neutral000, + todayContentColor = AppTheme.colors.neutral600, + todayDateBorderColor = AppTheme.colors.primary600 + ), + title = { Text(text = text) }, + showModeToggle = true + ) + } + } + } + } + } +} + +@Composable +private fun DateRangeSection(selected: Boolean, text: String, onSelect: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelect() + } + ) { + Row( + modifier = Modifier.padding( + horizontal = PaddingDefaults.XLarge, + vertical = PaddingDefaults.Medium + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = AppTheme.typography.body1 + ) + Spacer(modifier = Modifier.weight(1f)) + if (selected) { + Icon(Icons.Sharp.Check, null, tint = AppTheme.colors.primary600) + } + } + } +} + +@Composable +private fun ScheduleDateSection(text: String, date: LocalDate, onclickDate: () -> Unit) { + Row( + modifier = Modifier.padding( + horizontal = PaddingDefaults.XLarge, + vertical = PaddingDefaults.Medium + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = AppTheme.typography.body1 + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = date.formattedString(), + modifier = Modifier + .background( + shape = RoundedCornerShape(SizeDefaults.one), + color = AppTheme.colors.primary200 + ) + .clip(RoundedCornerShape(SizeDefaults.one)) + .clickable { + onclickDate() + } + .padding( + horizontal = PaddingDefaults.Tiny, + vertical = SizeDefaults.threeQuarter + ), + style = AppTheme.typography.body1 + ) + } +} + +@LightDarkPreview +@Composable +fun ScheduleDateRangeScreenPreview( + @PreviewParameter(ScheduleDateRangeScreenPreviewParameter::class) previewData: ScheduleDateRangeScreenPreview +) { + PreviewAppTheme { + val listState = rememberLazyListState() + + ScheduleDateRangeScreenScaffold( + listState = listState, + prescriptionScheduleState = previewData.state, + onChangeScheduledDate = {}, + onClickEndlessDateRange = {}, + onSelectIndividualDateRange = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationListScheduleScreenPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationListScheduleScreenPreviewParameter.kt new file mode 100644 index 00000000..49a53a80 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationListScheduleScreenPreviewParameter.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.PROFILE1 +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.PROFILE2 +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.LocalTime + +data class MedicationListScheduleScreenPreview( + val name: String, + val state: UiState> +) + +class MedicationListScheduleScreenPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MedicationListScheduleScreenPreview( + name = "error state", + state = UiState.Error(Throwable("test error")) + ), + MedicationListScheduleScreenPreview( + name = "empty state", + state = UiState.Empty() + ), + MedicationListScheduleScreenPreview( + name = "data state one profile with notifications", + state = UiState.Data( + listOf( + ProfileWithSchedules( + PROFILE1.toModel(), + medicationSchedules = listOf( + SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE + ) + ) + ) + ) + ), + MedicationListScheduleScreenPreview( + name = "data state two profiles with notifications", + state = UiState.Data( + listOf( + ProfileWithSchedules( + PROFILE1.toModel(), + medicationSchedules = listOf( + SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE + ) + ), + ProfileWithSchedules( + PROFILE2.toModel(), + medicationSchedules = listOf( + @Suppress("MagicNumber") + SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE.medicationSchedule.copy( + notifications = listOf( + MedicationNotification( + dosage = MedicationDosage("TAB", "1"), + time = LocalTime(12, 0) + ) + ) + ) + ) + ) + ) + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationNotificationSuccessScreenPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationNotificationSuccessScreenPreviewParameter.kt new file mode 100644 index 00000000..36261985 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationNotificationSuccessScreenPreviewParameter.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.PROFILE1 +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.PROFILE2 +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalTime + +data class MedicationSuccessScreenPreview( + val name: String, + val state: UiState>, + val currentTime: Instant = medicationPlanPreviewCurrentTime +) + +class MedicationSuccessScreenPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MedicationSuccessScreenPreview( + name = "error state", + state = UiState.Error(Throwable("test error")) + ), + MedicationSuccessScreenPreview( + name = "empty state", + state = UiState.Empty() + ), + MedicationSuccessScreenPreview( + name = "data state one profile with notifications", + state = UiState.Data( + listOf( + ProfileWithSchedules( + PROFILE1.toModel(), + medicationSchedules = listOf( + SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE + ) + ) + ) + ) + ), + MedicationSuccessScreenPreview( + name = "data state two profiles with notifications", + state = UiState.Data( + listOf( + ProfileWithSchedules( + PROFILE1.toModel(), + medicationSchedules = listOf( + SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE + ) + ), + ProfileWithSchedules( + PROFILE2.toModel(), + medicationSchedules = listOf( + @Suppress("MagicNumber") + SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE.medicationSchedule.copy( + notifications = listOf( + MedicationNotification( + dosage = MedicationDosage("TAB", "1"), + time = LocalTime(12, 0) + ) + ) + ) + ) + ) + ) + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanDosageInfoPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanDosageInfoPreviewParameter.kt new file mode 100644 index 00000000..82d8d456 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanDosageInfoPreviewParameter.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UnusedPrivateProperty") + +package de.gematik.ti.erp.app.medicationplan.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction + +data class MedicationPlanDosageInfoPreview( + val name: String, + val dosageInstruction: MedicationPlanDosageInstruction +) + +class MedicationPlanDosageInfoPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MedicationPlanDosageInfoPreview( + name = "free text dosage instruction", + dosageInstruction = PREVIEW_FREETEXT_DOSAGE_INSTRUCTION + ), + MedicationPlanDosageInfoPreview( + name = "empty dosage instruction", + dosageInstruction = PREVIEW_EMPTY_DOSAGE_INSTRUCTION + ), + MedicationPlanDosageInfoPreview( + name = "external dosage instruction", + dosageInstruction = PREVIEW_EXTERNAL_DOSAGE_INSTRUCTION + ), + MedicationPlanDosageInfoPreview( + name = "structured dosage instruction one in morning", + dosageInstruction = PREVIEW_STRUCTURED_DOSAGE_INSTRUCTION_ONE_IN_MORNING + ), + MedicationPlanDosageInfoPreview( + name = "structured dosage instruction two in all day times", + dosageInstruction = PREVIEW_STRUCTURED_DOSAGE_INSTRUCTION_TWO_IN_ALL_DAY_TIMES + ) + ) +} + +private val PREVIEW_FREETEXT_DOSAGE_INSTRUCTION = MedicationPlanDosageInstruction.FreeText( + text = "Take 1 tablet every 8 hours" +) + +private val PREVIEW_EMPTY_DOSAGE_INSTRUCTION = MedicationPlanDosageInstruction.Empty + +private val PREVIEW_EXTERNAL_DOSAGE_INSTRUCTION = MedicationPlanDosageInstruction.External + +private val PREVIEW_STRUCTURED_DOSAGE_INSTRUCTION_ONE_IN_MORNING = MedicationPlanDosageInstruction.Structured( + text = "1-0-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) +) + +private val PREVIEW_STRUCTURED_DOSAGE_INSTRUCTION_TWO_IN_ALL_DAY_TIMES = MedicationPlanDosageInstruction.Structured( + text = "2-2-2-2", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "2", + MedicationPlanDosageInstruction.DayTime.NOON to "2", + MedicationPlanDosageInstruction.DayTime.EVENING to "2", + MedicationPlanDosageInstruction.DayTime.NIGHT to "2" + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanScheduleScreenPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanScheduleScreenPreviewParameter.kt new file mode 100644 index 00000000..ea092e31 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/MedicationPlanScheduleScreenPreviewParameter.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.medicationplan.presentation.PrescriptionSchedule +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.ACTIVE_SYNCED_PRESCRIPTION_SCHEDULE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE_ENDLESS +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_PRESCRIPTION_SCHEDULE_INACTIVE +import de.gematik.ti.erp.app.utils.toLocalDate +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate + +val medicationPlanPreviewCurrentTime = Instant.parse("2023-01-01T16:20:00Z") + +data class MedicationScheduleScreenPreview( + val name: String, + val state: UiState, + val currentDate: LocalDate = medicationPlanPreviewCurrentTime.toLocalDate(), + val isIgnoringBatteryOptimizations: Boolean = true +) + +class MedicationPlanScheduleScreenPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + MedicationScheduleScreenPreview( + name = "error state", + state = UiState.Error(Throwable("test error")), + isIgnoringBatteryOptimizations = false + ), + MedicationScheduleScreenPreview( + name = "scanned prescription schedule inactive", + state = UiState.Data(SCANNED_PRESCRIPTION_SCHEDULE_INACTIVE) + ), + MedicationScheduleScreenPreview( + name = "scanned prescription schedule active", + state = UiState.Data(SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE) + ), + MedicationScheduleScreenPreview( + name = "synced prescription schedule active", + state = UiState.Data(ACTIVE_SYNCED_PRESCRIPTION_SCHEDULE), + isIgnoringBatteryOptimizations = false + ), + MedicationScheduleScreenPreview( + name = "synced prescription structured schedule active", + state = UiState.Data(ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE) + ), + MedicationScheduleScreenPreview( + name = "synced prescription structured schedule active endless", + state = UiState.Data(ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE_ENDLESS) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/ScheduleDateRangeScreenPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/ScheduleDateRangeScreenPreviewParameter.kt new file mode 100644 index 00000000..df2edaa3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/ScheduleDateRangeScreenPreviewParameter.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.medicationplan.presentation.PrescriptionSchedule +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE +import de.gematik.ti.erp.app.medicationplan.ui.preview.mocks.SCANNED_TASK_SCHEDULE +import de.gematik.ti.erp.app.utils.maxLocalDate +import de.gematik.ti.erp.app.utils.toLocalDate +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.LocalDate +import kotlin.time.Duration.Companion.days + +data class ScheduleDateRangeScreenPreview( + val name: String, + val state: UiState, + val currentDate: LocalDate = medicationPlanPreviewCurrentTime.toLocalDate() +) + +class ScheduleDateRangeScreenPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + ScheduleDateRangeScreenPreview( + name = "error state", + state = UiState.Error(Throwable("test error")) + ), + ScheduleDateRangeScreenPreview( + name = "scheduled endless", + state = UiState.Data( + SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE.copy( + medicationSchedule = SCANNED_TASK_SCHEDULE.copy( + start = medicationPlanPreviewCurrentTime.toLocalDate(), + end = maxLocalDate() + ) + ) + ) + ), + ScheduleDateRangeScreenPreview( + name = "scheduled individual", + state = UiState.Data( + SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE.copy( + medicationSchedule = SCANNED_TASK_SCHEDULE.copy( + start = medicationPlanPreviewCurrentTime.toLocalDate(), + end = medicationPlanPreviewCurrentTime.plus(10.days).toLocalDate() + ) + ) + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ProfileMocks.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ProfileMocks.kt new file mode 100644 index 00000000..eb50bb99 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ProfileMocks.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview.mocks + +import de.gematik.ti.erp.app.medicationplan.ui.preview.medicationPlanPreviewCurrentTime +import de.gematik.ti.erp.app.profiles.model.ProfilesData + +val PROFILE1 = ProfilesData.Profile( + id = "PROFILE_ID1", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = null, + name = "Erna Mustermann", + insurantName = "Erna Mustermann", + insuranceIdentifier = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + lastAuthenticated = medicationPlanPreviewCurrentTime, + lastTaskSynced = medicationPlanPreviewCurrentTime, + active = true, + singleSignOnTokenScope = null +) + +val PROFILE2 = PROFILE1.copy( + id = "PROFILE_ID2", + name = "Max Mustermann", + avatar = ProfilesData.Avatar.Grandfather +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScannedTaskDataMocks.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScannedTaskDataMocks.kt new file mode 100644 index 00000000..040b3eea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScannedTaskDataMocks.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview.mocks + +import de.gematik.ti.erp.app.medicationplan.ui.preview.medicationPlanPreviewCurrentTime +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData + +val SCANNED_TASK = ScannedTaskData.ScannedTask( + profileId = "PROFILE_ID", + taskId = "active-scanned-task-id-1", + index = 0, + name = "Scanned Task", + accessCode = "1234", + scannedOn = medicationPlanPreviewCurrentTime, + redeemedOn = null, + communications = emptyList() +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScheduleMocks.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScheduleMocks.kt new file mode 100644 index 00000000..33d132b0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/ScheduleMocks.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview.mocks + +import de.gematik.ti.erp.app.medicationplan.presentation.PrescriptionSchedule +import de.gematik.ti.erp.app.medicationplan.ui.preview.medicationPlanPreviewCurrentTime +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.model.toMedicationSchedule +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.utils.maxLocalDate + +val SCANNED_TASK_SCHEDULE = PrescriptionData.Scanned( + SCANNED_TASK +).toMedicationSchedule(medicationPlanPreviewCurrentTime) + +val SCANNED_PRESCRIPTION_SCHEDULE_INACTIVE = PrescriptionSchedule( + prescription = PrescriptionData.Scanned(SCANNED_TASK), + dosageInstruction = MedicationPlanDosageInstruction.Empty, + medicationSchedule = SCANNED_TASK_SCHEDULE +) + +val SCANNED_PRESCRIPTION_SCHEDULE_ACTIVE = PrescriptionSchedule( + prescription = PrescriptionData.Scanned(SCANNED_TASK), + dosageInstruction = MedicationPlanDosageInstruction.Empty, + medicationSchedule = SCANNED_TASK_SCHEDULE.copy(isActive = true) +) + +val SYNCED_PRESCRIPTION_SCHEDULE = PrescriptionData.Synced(SYNCED_TASK).toMedicationSchedule(medicationPlanPreviewCurrentTime) + +val SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE = PrescriptionData.Synced( + SYNCED_TASK_STRUCTURED_DOSAGE +).toMedicationSchedule(medicationPlanPreviewCurrentTime) + +val ACTIVE_SYNCED_PRESCRIPTION_SCHEDULE = PrescriptionSchedule( + prescription = PrescriptionData.Synced(SYNCED_TASK), + dosageInstruction = MedicationPlanDosageInstruction.FreeText("Dosage"), + medicationSchedule = SYNCED_PRESCRIPTION_SCHEDULE.copy(isActive = true) +) + +val ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE = PrescriptionSchedule( + prescription = PrescriptionData.Synced(SYNCED_TASK), + dosageInstruction = MedicationPlanDosageInstruction.Structured( + "1-0-1-0", + mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + medicationSchedule = SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE.copy(isActive = true) +) + +val ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE_ENDLESS = ACTIVE_SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE.copy( + medicationSchedule = SYNCED_PRESCRIPTION_STRUCTURED_SCHEDULE.copy( + isActive = true, + end = maxLocalDate() + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/SyncedTaskDataMocks.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/SyncedTaskDataMocks.kt new file mode 100644 index 00000000..7ceda40c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/ui/preview/mocks/SyncedTaskDataMocks.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui.preview.mocks + +import de.gematik.ti.erp.app.medicationplan.ui.preview.medicationPlanPreviewCurrentTime +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Medication +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.days + +private val ADDRESS = SyncedTaskData.Address( + line1 = "Hauptstraße 1", + line2 = "12345 Musterstadt", + postalCode = "12345", + city = "Musterstadt" +) + +internal val PATIENT = Patient( + name = "Erna Mustermann", + address = ADDRESS, + birthdate = null, + insuranceIdentifier = "AOK" +) + +private val MEDICATION = Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Medication", + form = "AEO", + lotNumber = "123456", + expirationDate = FhirTemporal.Instant(Clock.System.now().plus(30.days)), + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "KA", + amount = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ), + manufacturingInstructions = null, + packaging = null, + ingredientMedications = emptyList(), + ingredients = emptyList() +) + +private val MEDICATION_10_TAB = MEDICATION.copy( + form = "TAB", + amount = Ratio( + numerator = Quantity( + value = "10", + unit = "TAB" + ), + denominator = null + ) +) + +internal var MEDICATION_REQUEST = MedicationRequest( + medication = MEDICATION, + dateOfAccident = null, + location = "Location", + emergencyFee = true, + dosageInstruction = "Dosage", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "Note", + substitutionAllowed = true +) + +internal var MEDICATION_REQUEST_DOSAGE_STRUCTURED_AMOUNT_20 = MEDICATION_REQUEST.copy( + medication = MEDICATION_10_TAB, + dosageInstruction = "1-0-1-0" +) + +internal val PRACTITIONER = Practitioner( + name = "Dr. Max Mustermann", + qualification = "Arzt", + practitionerIdentifier = "1234567890" +) + +internal val INSURANCE_INFO = SyncedTaskData.InsuranceInformation( + name = "AOK", + status = "status", + coverageType = SyncedTaskData.CoverageType.GKV +) + +internal val ORGANIZATION = Organization( + name = "Praxis Dr. Mustermann", + address = ADDRESS, + uniqueIdentifier = "1234567890", + phone = "0123456789", + mail = "mustermann@praxis.de" +) + +val SYNCED_TASK = SyncedTaskData.SyncedTask( + profileId = "PROFILE_ID", + taskId = "active-synced-task-id-1", + accessCode = "1234", + lastModified = medicationPlanPreviewCurrentTime, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFO, + expiresOn = medicationPlanPreviewCurrentTime.plus(30.days), + acceptUntil = medicationPlanPreviewCurrentTime.plus(27.days), + authoredOn = medicationPlanPreviewCurrentTime, + status = SyncedTaskData.TaskStatus.Ready, + isIncomplete = false, + pvsIdentifier = "pvsIdentifier", + failureToReport = "failureToReport", + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = emptyList(), + lastMedicationDispense = null, + communications = emptyList() +) + +val SYNCED_TASK_STRUCTURED_DOSAGE = SYNCED_TASK.copy( + medicationRequest = MEDICATION_REQUEST_DOSAGE_STRUCTURED_AMOUNT_20 +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/PlanMedicationScheduleUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/PlanMedicationScheduleUseCase.kt new file mode 100644 index 00000000..dca23985 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/PlanMedicationScheduleUseCase.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import android.content.Context +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.worker.scheduleReminderWorker +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.time.Duration + +data class ScheduleReminderWorker(val context: Context) { + fun schedule() { + context.scheduleReminderWorker(Duration.ZERO) + } +} + +class PlanMedicationScheduleUseCase( + private val scheduler: ScheduleReminderWorker, + private val medicationPlanRepository: MedicationPlanRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(medicationSchedule: MedicationSchedule) = + withContext(dispatcher) { + medicationPlanRepository.updateMedicationSchedule(medicationSchedule) + scheduler.schedule() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/worker/PlanMedicationScheduleWorker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/worker/PlanMedicationScheduleWorker.kt new file mode 100644 index 00000000..093d7e2f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/medicationplan/worker/PlanMedicationScheduleWorker.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.worker + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.compose.runtime.Stable +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationChannelGroupCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.medicationplan.presentation.checkNotificationPermission +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.usecase.LoadAllMedicationSchedulesUseCase +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.first +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atDate +import kotlinx.datetime.plus +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.android.subDI +import org.kodein.di.instance +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + +private val LookaheadDuration = 12.hours +private val NotificationsInRangeDuration = 5.minutes +private const val REMINDER_NOTIFICATION_GROUP = "ReminderNotificationGroup" +private const val REMINDER_NOTIFICATION = "ShareNotificationAcceptRequest" +private val ReminderNotificationId = "ReminderNotificationId".hashCode() +const val REMINDER_NOTIFICATION_INTENT_ACTION = "de.gematik.erp.app.ReminderNotificationIntentAction" + +/** + * A CoroutineWorker that handles scheduling and showing medication reminders. + * + * @property di Dependency injection property. + * @property loadAllMedicationSchedulesUseCase Use case to load all medication schedules. + */ +class PlanMedicationScheduleWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params), DIAware { + override val di by context.subDI(closestDI()) {} + private val loadAllMedicationSchedulesUseCase by instance() + + /** + * Performs the work to schedule and show medication reminders. + * It loads all medication schedules and checks for notifications in the range of NotificationsInRangeDuration. + * If there are notifications, it cancels the previous reminder notification and shows the new one. + * If there are no notifications, it schedules the next reminder worker with LookaheadDuration. + * + * @return Listenable Result of the work. + */ + + override suspend fun doWork(): Result = + runCatching { + val now = Clock.System.now() + val timeZone = TimeZone.currentSystemDefault() + val schedules = loadAllMedicationSchedulesUseCase().first() + + val currentNotifications = schedules.nextNotificationsInRange( + (now - NotificationsInRangeDuration).toLocalDateTime(timeZone)..(now + NotificationsInRangeDuration).toLocalDateTime(timeZone) + ) + + val nextNotifications = schedules.nextNotificationsInRange( + (now + NotificationsInRangeDuration).toLocalDateTime(timeZone)..(now + LookaheadDuration).toLocalDateTime(timeZone) + ) + + if (nextNotifications.isEmpty()) { + applicationContext.scheduleReminderWorker(LookaheadDuration) + } else { + applicationContext.scheduleReminderWorker( + nextNotifications.first().notificationDateTime.toInstant( + TimeZone.currentSystemDefault() + ) - now + ) + } + + if (currentNotifications.isNotEmpty()) { + applicationContext.cancelReminderNotification() + applicationContext.showReminderNotification() + } + }.fold( + onSuccess = { + Result.success() + }, + onFailure = { + Napier.e(it) { "Reminder worker failed" } + Result.failure() + } + ) + + companion object { + const val TAG = "PlanMedicationScheduleWorker" + } +} + +/** + * Data class representing a notification with its schedule and date-time. + * + * @property schedule The medication schedule. + * @property notification The medication notification. + * @property notificationDateTime The date and time of the notification. + */ +@Stable +data class DateTimeNotification( + val schedule: MedicationSchedule, + val notification: MedicationNotification, + val notificationDateTime: LocalDateTime +) + +/** + * Extension function to get the next notifications from a list of scheduled medications in a given range. + * + * @param range The range of LocalDateTime to check for notifications. + * @return List of DateTimeNotification within the range. + */ +fun List.nextNotificationsInRange(range: ClosedRange): List = + asSequence() + .filter { it.isActive && it.notifications.isNotEmpty() } + .notificationsInRange(range) + .sortedBy { (_, _, dateTime) -> dateTime } + .toList() + +/** + * Extension function to get notifications in a given range from a sequence of MedicationSchedule. + * + * @param range The range of LocalDateTime to check for notifications. + * @return Sequence of DateTimeNotification within the range. + */ +fun Sequence.notificationsInRange( + range: ClosedRange +): Sequence = + flatMap { schedule -> schedule.notificationsInRange(range) } + +/** + * Extension function to get notifications in a given range from a MedicationSchedule. + * + * @param range The range of LocalDateTime to check for notifications. + * @return Sequence of DateTimeNotification within the range. + */ +fun MedicationSchedule.notificationsInRange( + range: ClosedRange +): Sequence { + val startDate = range.start.date + val endDate = range.endInclusive.date + + return sequence { + var date = startDate + do { + this@notificationsInRange.notifications.forEach { + yield(DateTimeNotification(this@notificationsInRange, it, it.time.atDate(date))) + } + date = date.plus(1, DateTimeUnit.DAY) + } while (date <= endDate) + }.filter { (_, _, dateTime) -> dateTime in range } +} + +/** + * Extension function to cancel the reminder notification. + */ +fun Context.cancelReminderNotification() { + NotificationManagerCompat.from(this).cancel(ReminderNotificationId) +} + +/** + * Extension function to schedule a reminder worker with a given duration. + * + * @param duration The duration to delay the worker. + */ +fun Context.scheduleReminderWorker( + duration: Duration +) { + val request = + OneTimeWorkRequest + .Builder(PlanMedicationScheduleWorker::class.java) + .setInitialDelay(duration.toJavaDuration()) + .addTag(PlanMedicationScheduleWorker.TAG) + .build() + + WorkManager + .getInstance(this) + .apply { + cancelAllWorkByTag(PlanMedicationScheduleWorker.TAG) + enqueue(request) + } +} + +/** + * Extension function to create a notification reminder group. + */ +fun Context.createNotificationReminderGroup() { + val group = + NotificationChannelGroupCompat + .Builder(REMINDER_NOTIFICATION_GROUP) + .setName(R.string.notification_header.toString()) + .build() + + NotificationManagerCompat.from(this).createNotificationChannelGroup(group) +} + +/** + * Extension function to create a notification reminder channel. + */ +fun Context.createNotificationReminderChannel() { + val channel = + NotificationChannelCompat.Builder( + REMINDER_NOTIFICATION, + NotificationManagerCompat.IMPORTANCE_MAX + ) + .setName(R.string.notification_header.toString()) + .setGroup(REMINDER_NOTIFICATION_GROUP) + .build() + + NotificationManagerCompat.from(this).createNotificationChannel(channel) +} + +/** + * Extension function to show a reminder notification with a list of notifications. + * (shows the notifications which was planned in NotificationsInRangeDuration) + * + * @param notifications The list of DateTimeNotification to show. + */ +@Requirement( + "O.Plat_4#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Reminder notifications do not include any sensitive data, only static text.", + codeLines = 50 +) +fun Context.showReminderNotification() { + val openAppIntent = + openAppIntent( + action = REMINDER_NOTIFICATION_INTENT_ACTION, + extras = Bundle.EMPTY + ) + + val notification = + NotificationCompat.Builder(this, REMINDER_NOTIFICATION) + .setSmallIcon(R.drawable.ic_logo_outlined) + .setContentTitle(getString(R.string.notification_header)) + .setContentText(getString(R.string.notification_text)) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setContentIntent(openAppIntent) + .setAutoCancel(true) + .addAction(R.drawable.ic_logo_outlined, getString(R.string.notification_action_open_app), openAppIntent) + .build() + @Requirement( + "O.Plat_5#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for notification permission and notify if granted.", + codeLines = 50 + ) + checkNotificationPermission( + onGranted = { + NotificationManagerCompat.from(this).notify(ReminderNotificationId, notification) + }, + onDenied = {} + ) +} + +/** + * Extension function to create an intent to open the app. + * + * @param action The action for the intent. + * @param extras The extras to add to the intent. + * @return PendingIntent to open the app. + */ +fun Context.openAppIntent( + action: String, + extras: Bundle +): PendingIntent { + val intent = + Intent(this, MainActivity::class.java).apply { + this.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + this.action = action + this.putExtras(extras) + } + return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/di/MessagesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/di/MessagesModule.kt new file mode 100644 index 00000000..950fec58 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/di/MessagesModule.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.di + +import de.gematik.ti.erp.app.base.usecase.UpdateInAppMessageUseCase +import de.gematik.ti.erp.app.changelogs.DefaultInAppMessageRepository +import de.gematik.ti.erp.app.changelogs.InAppDataSource +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.messages.domain.repository.InAppLocalMessageRepository +import de.gematik.ti.erp.app.messages.domain.usecase.FetchInAppMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessageUsingOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessagesUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetProfileByOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetRepliedMessagesUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetUnreadMessagesCountUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.FetchWelcomeMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.SetInternalMessageAsReadUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByOrderIdAndCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateInvoicesByOrderIdAndTaskIdUseCase +import de.gematik.ti.erp.app.messages.repository.CommunicationLocalDataSource +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.repository.DefaultCommunicationRepository +import de.gematik.ti.erp.app.messages.repository.PharmacyCacheLocalDataSource +import de.gematik.ti.erp.app.messages.repository.PharmacyCacheRemoteDataSource +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val messagesModule = DI.Module("messagesModule") { + bindProvider { PharmacyCacheLocalDataSource(instance()) } + bindProvider { PharmacyCacheRemoteDataSource(instance()) } + bindProvider { CommunicationLocalDataSource(instance()) } + bindProvider { GetRepliedMessagesUseCase(instance(), instance()) } + bindProvider { GetMessagesUseCase(instance(), instance(), instance()) } + bindProvider { GetMessageUsingOrderIdUseCase(instance(), instance()) } + bindProvider { GetProfileByOrderIdUseCase(instance()) } + bindProvider { GetUnreadMessagesCountUseCase(instance(), instance(), instance()) } + bindProvider { UpdateCommunicationByOrderIdAndCommunicationIdUseCase(instance(), instance()) } + bindProvider { UpdateCommunicationByCommunicationIdUseCase(instance()) } + bindProvider { UpdateInvoicesByOrderIdAndTaskIdUseCase(instance(), instance()) } + bindProvider { UpdateInAppMessageUseCase(instance(), instance(), instance()) } + bindProvider { InAppMessageResources(instance()) } + bindProvider { InAppLocalMessageRepository(instance()) } + bindProvider { FetchWelcomeMessageUseCase(instance(), instance(), instance()) } + bindProvider { FetchInAppMessageUseCase(instance(), instance(), instance()) } + bindProvider { SetInternalMessageAsReadUseCase(instance()) } +} + +val messageRepositoryModule = DI.Module("messageRepositoryModule", allowSilentOverride = true) { + bindProvider { + DefaultCommunicationRepository( + instance(), + instance(), + instance(), + instance(), + instance(), + instance() + ) + } + bindProvider { InAppDataSource(instance()) } + bindProvider { DefaultInAppMessageRepository(instance()) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageData.kt new file mode 100644 index 00000000..535e985a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageData.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.model + +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData.LastMessage +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +@Serializable +data class InAppMessage( + val id: String, + val from: String, + val text: String?, + val timestamp: Instant, + val prescriptionsCount: Int = 0, + val tag: String, + var isUnread: Boolean, + val lastMessage: LastMessage?, + val messageProfile: CommunicationProfile?, + val version: String +) + +@Serializable +data class LocalInAppJsonMessage( + val id: String, + val timestamp: String?, + val text: String?, + val version: String +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageResources.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageResources.kt new file mode 100644 index 00000000..b3675ca5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/InAppMessageResources.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.model + +import android.content.Context +import de.gematik.ti.erp.app.features.R + +class InAppMessageResources( + private val context: Context +) { + val language = context.resources.configuration.locales[0].language + val assets = context.assets + val messageFrom = context.getString(R.string.internal_message_from) + fun getMessageTag(version: String): String = context.getString(R.string.internal_message_tag, version) + + val welcomeMessage = context.getString(R.string.welcome_text) + val welcomeMessageTag = context.getString(R.string.welcome_tag) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/OrderUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/OrderUseCaseData.kt new file mode 100644 index 00000000..d64ed494 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/model/OrderUseCaseData.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.model + +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +object OrderUseCaseData { + @Serializable + data class Pharmacy( + val id: String, + val name: String + ) + + @Serializable + data class Order( + val orderId: String, + val prescriptions: List, + val sentOn: Instant, + val pharmacy: Pharmacy, + val hasUnreadMessages: Boolean, + val latestCommunicationMessage: LastMessage? + ) + + @Serializable + data class OrderDetail( + val orderId: String, + val taskDetailedBundles: List, + val sentOn: Instant, + val pharmacy: Pharmacy, + val hasUnreadMessages: Boolean = false + ) + + @Serializable + data class InvoiceInfo( + val hasInvoice: Boolean = false, + val invoiceSentOn: Instant? = null + ) + + @Serializable + data class TaskDetailedBundle( + val invoiceInfo: InvoiceInfo = InvoiceInfo(), + val prescription: Prescription? + ) + + @Serializable + data class Message( + val communicationId: String, + val sentOn: Instant, + val message: String?, + val additionalInfo: String = "", + val pickUpCodeDMC: String?, + val pickUpCodeHR: String?, + val link: String?, + val consumed: Boolean + ) { + enum class Type { + All, + Link, + PickUpCodeDMC, + PickUpCodeHR, + Text, + Empty + } + + val type: Type = determineMessageType() + + private fun determineMessageType(): Type { + val filledFieldsCount = listOfNotNull(link, pickUpCodeDMC, pickUpCodeHR, message).size + + return when { + filledFieldsCount == 0 -> Type.Empty + filledFieldsCount > 1 -> Type.All + link != null -> Type.Link + pickUpCodeDMC != null -> Type.PickUpCodeDMC + pickUpCodeHR != null -> Type.PickUpCodeHR + message != null -> Type.Text + else -> Type.Empty + } + } + } + + @Serializable + data class LastMessage( + val lastMessageDetails: LastMessageDetails, + val profile: CommunicationProfile + ) + + @Serializable + data class LastMessageDetails( + val message: String?, + val pickUpCodeDMC: String?, + val pickUpCodeHR: String?, + val link: String? + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/repository/InAppMessageRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/repository/InAppMessageRepository.kt new file mode 100644 index 00000000..40a3422d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/repository/InAppMessageRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.repository + +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.messages.domain.model.LocalInAppJsonMessage +import de.gematik.ti.erp.app.messages.mappers.toInAppMessage +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.serialization.json.Json + +class InAppLocalMessageRepository( + private val messageResources: InAppMessageResources +) { + fun getInternalMessages(): Flow> = flow { + val language = messageResources.language // Get device language + val fileName = "$language.lproj/internal_messages.json" // Construct path to file in assets + emit( + try { + val jsonString = messageResources.assets.open(fileName).bufferedReader().use { it.readText() } + val message: List = Json.decodeFromString>(jsonString) + message.map { + it.toInAppMessage( + messageResources.messageFrom, + messageResources.getMessageTag(it.version), + it.timestamp?.let { time -> Instant.parse(time) } ?: Clock.System.now() + ) + } + } catch (e: Exception) { + Napier.e("Error reading internal messages: ${e.stackTraceToString()}") + emptyList() + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/DispenseRequestCommunicationToOrder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/DispenseRequestCommunicationToOrder.kt new file mode 100644 index 00000000..465b22fc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/DispenseRequestCommunicationToOrder.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.prescription.mapper.toPrescription +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +suspend fun Communication.dispenseRequestCommunicationToOrder( + communicationRepository: CommunicationRepository, + invoiceRepository: InvoiceRepository, + withMedicationNames: Boolean, + dispatcher: CoroutineDispatcher = Dispatchers.IO +): Flow, Boolean>> = flow { + val taskIds: List = communicationRepository.taskIdsByOrder(orderId).first() + val hasUnreadDispenseMessage = communicationRepository.hasUnreadDispenseMessage(taskIds, orderId).first() + val hasUnreadMessages: Boolean = communicationRepository.hasUnreadRepliedMessages(taskIds = taskIds, telematikId = recipient).first() + val hasUnreadInvoices: Boolean = invoiceRepository.hasUnreadInvoiceMessages(taskIds).first() + + val prescriptions = if (withMedicationNames) { + taskIds.map { + communicationRepository.loadSyncedByTaskId(it).first()?.toPrescription() + ?: communicationRepository.loadScannedByTaskId(it).first()?.toPrescription() + } + } else { + emptyList() + } + + emit(Pair(prescriptions, hasUnreadDispenseMessage || hasUnreadMessages || hasUnreadInvoices)) +}.flowOn(dispatcher) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchInAppMessageUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchInAppMessageUseCase.kt new file mode 100644 index 00000000..882cf576 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchInAppMessageUseCase.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.messages.domain.repository.InAppLocalMessageRepository +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Instant + +class FetchInAppMessageUseCase( + private val inAppMessageRepository: InAppMessageRepository, + private val localMessageRepository: InAppLocalMessageRepository, + private val messageResources: InAppMessageResources + +) { + suspend operator fun invoke(): Flow> { + val showWelcomeMessage = inAppMessageRepository.showWelcomeMessage.first() + val inAppMessageEntities = inAppMessageRepository.inAppMessages.first() + val internalMessages = localMessageRepository.getInternalMessages() + return internalMessages.map { + it.drop(if (showWelcomeMessage) 1 else 0) + .map { inAppMessage -> + InAppMessage( + id = inAppMessage.id, + from = messageResources.messageFrom, + timestamp = Instant.parse(inAppMessage.timestamp.toString()), + text = inAppMessage.text, + tag = messageResources.getMessageTag(inAppMessage.version), + version = inAppMessage.version, + isUnread = inAppMessageEntities.find { it.id == inAppMessage.id }?.isUnRead ?: false, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + prescriptionsCount = 0 + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchWelcomeMessageUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchWelcomeMessageUseCase.kt new file mode 100644 index 00000000..f30f1caf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/FetchWelcomeMessageUseCase.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import androidx.annotation.VisibleForTesting +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +@OptIn(ExperimentalCoroutinesApi::class) +class FetchWelcomeMessageUseCase( + private val inAppMessageRepository: InAppMessageRepository, + private val buildConfigInformation: BuildConfigInformation, + private val messageResources: InAppMessageResources +) { + + operator fun invoke(): Flow = + inAppMessageRepository.showWelcomeMessage.flatMapLatest { showWelcomeMessage -> + when { + showWelcomeMessage -> generateWelcomeMessageFlow() + else -> flowOf(null) + } + } + + private fun generateWelcomeMessageFlow(): Flow = inAppMessageRepository.inAppMessages.flatMapLatest { inAppMessages -> + flow { + val versionWithoutRC = buildConfigInformation.versionName().substringBefore("-") + emit(createWelcomeMessage(versionWithoutRC, inAppMessages)) + } + } + + private fun createWelcomeMessage(version: String, inAppMessagesEntity: List): InAppMessage? = + inAppMessagesEntity.firstOrNull()?.let { + InAppMessage( + id = it.id, + from = messageResources.messageFrom, + text = messageResources.welcomeMessage, + timestamp = Instant.parse(getCurrentTimeAsString()), + tag = messageResources.welcomeMessageTag, + isUnread = it.isUnRead, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = version + ) + } + + @VisibleForTesting + fun getCurrentTimeAsString(): String { + val currentInstant = Clock.System.now() + val dateTime = currentInstant.toLocalDateTime(TimeZone.UTC) + return dateTime.toString().substring(0, TIMESTAMP_SUBSTRING_LENGTH) + "Z" + } + + companion object { + const val TIMESTAMP_SUBSTRING_LENGTH = 19 + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessageUsingOrderIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessageUsingOrderIdUseCase.kt new file mode 100644 index 00000000..eaa1376e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessageUsingOrderIdUseCase.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.mappers.toOrderDetail +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.mapper.toPrescription +import de.gematik.ti.erp.app.prescription.model.Communication +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn + +class GetMessageUsingOrderIdUseCase( + private val communicationRepository: CommunicationRepository, + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(orderId: String): Flow = + combine( + communicationRepository.loadDispReqCommunications(orderId), + communicationRepository.loadPharmacies() + ) { communications, pharmacies -> + communications.firstOrNull()?.let { communication -> + communication.dispenseRequestCommunicationToOrder( + communicationRepository = communicationRepository, + withMedicationNames = true, + pharmacyName = pharmacies.find { it.telematikId == communication.recipient }?.name + ) + } + }.flowOn(dispatcher) + + private suspend fun Communication.dispenseRequestCommunicationToOrder( + communicationRepository: CommunicationRepository, + withMedicationNames: Boolean, + pharmacyName: String? + ): OrderUseCaseData.OrderDetail { + val taskIds = communicationRepository.taskIdsByOrder(orderId).first() + val taskDetailedBundles = taskIds.map { + val invoice: InvoiceData.PKVInvoiceRecord? = invoiceRepository.invoiceByTaskId(it).first() + OrderUseCaseData.TaskDetailedBundle( + invoiceInfo = OrderUseCaseData.InvoiceInfo( + hasInvoice = invoice != null, + invoiceSentOn = invoice?.timestamp + ), + prescription = when { + withMedicationNames -> loadPrescription(it) + else -> null + } + ) + } + if (pharmacyName == null) { + communicationRepository.downloadMissingPharmacy(recipient) + } + + return toOrderDetail( + taskDetailedBundles = taskDetailedBundles, + pharmacyName = pharmacyName + ) + } + + private suspend fun loadPrescription(taskId: String) = + communicationRepository.loadSyncedByTaskId(taskId).first()?.toPrescription() + ?: communicationRepository.loadScannedByTaskId(taskId).first()?.toPrescription() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessagesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessagesUseCase.kt new file mode 100644 index 00000000..450cbcd9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetMessagesUseCase.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.mappers.toMessage +import de.gematik.ti.erp.app.messages.repository.CachedPharmacy +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext + +class GetMessagesUseCase( + private val communicationRepository: CommunicationRepository, + private val invoiceRepository: InvoiceRepository, + private val profileRepository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(): List = withContext(dispatcher) { + val profiles = profileRepository.profiles().first() + val pharmacies: List = communicationRepository.loadPharmacies().first() + + profiles.flatMap { profile -> + communicationRepository.loadFirstDispReqCommunications(profile.id).first() + .groupBy { it.taskId to it.orderId to it.recipient to it.payload } + .map { (_, communications) -> + val latestCommunication = communications.maxByOrNull { it.sentOn } + latestCommunication?.let { + mapCommunicationToOrder(it, pharmacies) + } + } + .filterNotNull() + }.sortedByDescending { it.sentOn } + } + + private suspend fun mapCommunicationToOrder( + communication: Communication, + pharmacies: List + ): OrderUseCaseData.Order { + val pharmacyName = pharmacies.getPharmacyName(communication) + val (prescriptions, hasUnreadMessages) = communication.dispenseRequestCommunicationToOrder( + communicationRepository = communicationRepository, + invoiceRepository = invoiceRepository, + withMedicationNames = true, + dispatcher = dispatcher + ).first() + + return OrderUseCaseData.Order( + orderId = communication.orderId, + prescriptions = prescriptions, + sentOn = communication.sentOn, + pharmacy = OrderUseCaseData.Pharmacy( + id = communication.recipient, // getting Pharmacy ID (telematikId) from communication.recipient + name = pharmacyName + ), + hasUnreadMessages = hasUnreadMessages, + latestCommunicationMessage = getLatestCommunicationMessage( + communication.orderId, + pharmacyName, + communication.recipient + ) + ) + } + + private suspend fun List.getPharmacyName(communication: Communication): String { + return try { + // Try to find the pharmacy in the existing list + val pharmacy = this.find { it.telematikId == communication.recipient } + + // If the pharmacy is found, return its name; otherwise, attempt to download and find it again + pharmacy?.name ?: run { + communicationRepository.downloadMissingPharmacy(communication.recipient).getOrNull()?.name ?: "" + } + } catch (e: Throwable) { + Napier.e { "error on getting pharmacy name ${e.message}" } + // Return empty string if any exception occurs + "" + } + } + + private suspend fun getLatestCommunicationMessage(orderId: String, pharmacyName: String, telematikId: String): OrderUseCaseData.LastMessage? = + supervisorScope { + val taskIds = communicationRepository.taskIdsByOrder(orderId).first() + + return@supervisorScope combine( + communicationRepository.loadRepliedCommunications(taskIds, telematikId), + communicationRepository.loadDispReqCommunications(orderId) + ) { repliedCommunications, dispReqCommunications -> + repliedCommunications + dispReqCommunications + }.mapNotNull { combinedCommunication -> + val lastMessage = combinedCommunication.maxByOrNull { it.sentOn } + lastMessage?.let { + it.generateQuickMessage(pharmacyName)?.let { lastMessage -> + OrderUseCaseData.LastMessage( + lastMessageDetails = lastMessage, + profile = it.profile + ) + } + } + }.firstOrNull() + } + + private fun Communication?.generateQuickMessage(pharmacyName: String): OrderUseCaseData.LastMessageDetails? { + return when (this?.profile) { + CommunicationProfile.ErxCommunicationDispReq -> OrderUseCaseData.LastMessageDetails( + message = pharmacyName, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null + ) + + CommunicationProfile.ErxCommunicationReply -> { + val messageData = this.toMessage() + OrderUseCaseData.LastMessageDetails( + message = messageData.message, + pickUpCodeDMC = messageData.pickUpCodeDMC, + pickUpCodeHR = messageData.pickUpCodeHR, + link = messageData.link + ) + } + + else -> null + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetProfileByOrderIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetProfileByOrderIdUseCase.kt new file mode 100644 index 00000000..000a5bb2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetProfileByOrderIdUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +class GetProfileByOrderIdUseCase( + private val communicationRepository: CommunicationRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(orderId: String): Flow = + communicationRepository.profileByOrderId(orderId) + .mapNotNull { it.toModel() } + .flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetRepliedMessagesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetRepliedMessagesUseCase.kt new file mode 100644 index 00000000..36a94d9d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetRepliedMessagesUseCase.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.messages.mappers.toMessage +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.model.Communication +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class GetRepliedMessagesUseCase( + private val communicationRepository: CommunicationRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(orderId: String, telematikId: String): Flow> = + communicationRepository.taskIdsByOrder(orderId).flatMapLatest { taskIds -> + communicationRepository.loadRepliedCommunications(taskIds = taskIds, telematikId = telematikId) + .map { communications -> + Napier.d { "GetRepliedMessagesUseCase: communications: $communications" } + communications.map(Communication::toMessage) + } + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetUnreadMessagesCountUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetUnreadMessagesCountUseCase.kt new file mode 100644 index 00000000..81c150f6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/GetUnreadMessagesCountUseCase.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +class GetUnreadMessagesCountUseCase( + private val communicationRepository: CommunicationRepository, + private val inAppMessageRepository: InAppMessageRepository, + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(profileId: ProfileIdentifier): Flow { + return combine( + communicationRepository.unreadMessagesCount(consumed = false), + getUnreadInvoiceCount(profileId), + inAppMessageRepository.counter + ) { unreadMessagesCount, unreadInvoiceCount, counter -> + val totalCount = unreadMessagesCount + unreadInvoiceCount + counter + totalCount + }.flowOn(dispatcher) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getUnreadInvoiceCount(profileId: ProfileIdentifier): Flow { + return invoiceRepository.getInvoiceTaskIdAndConsumedStatus(profileId) + .flatMapLatest { invoiceStatusList -> + if (invoiceStatusList.isEmpty()) { + return@flatMapLatest flowOf(0L) + } + + val taskIdConsumedMap = invoiceStatusList.associate { it.taskId to it.consumed } + + val requestCommunicationFlow = communicationRepository.loadFirstDispReqCommunications(profileId) + val replyCommunicationFlow = communicationRepository.loadRepliedCommunications( + invoiceStatusList.map { it.taskId }, + "" + ) + + combine( + requestCommunicationFlow, + replyCommunicationFlow + ) { requestFlow, replyFlow -> + val unreadRequestTaskIds = filterUnreadTaskIds(requestFlow, taskIdConsumedMap) + val unreadReplyTaskIds = filterUnreadTaskIds(replyFlow, taskIdConsumedMap) + + // Combine both request and reply task IDs and remove duplicates + (unreadRequestTaskIds + unreadReplyTaskIds).toSet().size.toLong() + } + } + .onEmpty { + emit(0L) + } + } + + private fun filterUnreadTaskIds( + communicationFlow: List, + taskIdConsumedMap: Map + ): List { + return communicationFlow + .filter { it.taskId in taskIdConsumedMap && taskIdConsumedMap[it.taskId] == false } + .map { it.taskId } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SaveLocalCommunicationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SaveLocalCommunicationUseCase.kt new file mode 100644 index 00000000..9229171b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SaveLocalCommunicationUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SaveLocalCommunicationUseCase( + private val repository: CommunicationRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(taskId: String, pharmacyId: String, transactionId: String) { + withContext(dispatcher) { + repository.saveLocalCommunication(taskId, pharmacyId, transactionId) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SetInternalMessageIsReadUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SetInternalMessageIsReadUseCase.kt new file mode 100644 index 00000000..14d6433e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/SetInternalMessageIsReadUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository + +class SetInternalMessageAsReadUseCase( + private val inAppMessageRepository: InAppMessageRepository +) { + suspend operator fun invoke() { + inAppMessageRepository.setInternalMessageAsRead() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByCommunicationIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByCommunicationIdUseCase.kt new file mode 100644 index 00000000..2e526b58 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByCommunicationIdUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class UpdateCommunicationByCommunicationIdUseCase( + private val repository: CommunicationRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(communicationId: String) { + withContext(dispatcher) { + repository.setCommunicationStatus(communicationId, true) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByOrderIdAndCommunicationIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByOrderIdAndCommunicationIdUseCase.kt new file mode 100644 index 00000000..2e8538db --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateCommunicationByOrderIdAndCommunicationIdUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext + +class UpdateCommunicationByOrderIdAndCommunicationIdUseCase( + private val repository: CommunicationRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(orderId: String) { + withContext(dispatcher) { + repository.loadDispReqCommunications(orderId).firstOrNull()?.let { communications -> + communications.forEach { + repository.setCommunicationStatus(it.communicationId, consumed = true) + } + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateInvoicesByOrderIdAndTaskIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateInvoicesByOrderIdAndTaskIdUseCase.kt new file mode 100644 index 00000000..464d4681 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/domain/usecase/UpdateInvoicesByOrderIdAndTaskIdUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.domain.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +class UpdateInvoicesByOrderIdAndTaskIdUseCase( + private val communicationRepository: CommunicationRepository, + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(orderId: String) { + withContext(dispatcher) { + communicationRepository.loadDispReqCommunications(orderId).first().forEach { + invoiceRepository.updateInvoiceCommunicationStatus(it.taskId, consumed = true) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/CommunicationMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/CommunicationMapper.kt new file mode 100644 index 00000000..ef4b9472 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/CommunicationMapper.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.mappers + +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.pharmacy.repository.model.CommunicationPayloadInbox +import de.gematik.ti.erp.app.prescription.model.Communication +import io.github.aakira.napier.Napier +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +private val lenientJson = Json { + isLenient = true + ignoreUnknownKeys = true +} + +fun Communication.toOrderDetail( + taskDetailedBundles: List, + pharmacyName: String? +) = + OrderUseCaseData.OrderDetail( + orderId = orderId, + taskDetailedBundles = taskDetailedBundles, + sentOn = sentOn, + pharmacy = OrderUseCaseData.Pharmacy(name = pharmacyName ?: "", id = this.recipient) + ) + +fun Communication.toMessage(): OrderUseCaseData.Message { + val defaultValues = OrderUseCaseData.Message( + communicationId = communicationId, + sentOn = sentOn, + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = consumed + ) + + return payload?.let { nonNullPayload -> + try { + val inbox = lenientJson.decodeFromString(nonNullPayload) + OrderUseCaseData.Message( + communicationId = communicationId, + sentOn = sentOn, + message = inbox.infoText?.takeUnless { it.isBlank() }, + pickUpCodeDMC = inbox.pickUpCodeDMC?.takeUnless { it.isBlank() }, + pickUpCodeHR = inbox.pickUpCodeHR?.takeUnless { it.isBlank() }, + link = inbox.url?.takeUnless { it.isBlank() }?.takeIf { isValidUrl(it) }, + consumed = consumed + ) + } catch (ignored: SerializationException) { + Napier.d { "No payload, default message" } + defaultValues + } + } ?: run { + Napier.d { "No payload, default message" } + defaultValues + } +} + +/** + * Check if a URL is valid and uses the HTTPS scheme. + */ +fun isValidUrl(url: String): Boolean = + url.matches("^https://.*".toRegex()) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/InAppMessageMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/InAppMessageMapper.kt new file mode 100644 index 00000000..2d146a4c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/mappers/InAppMessageMapper.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.mappers + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.LocalInAppJsonMessage +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import io.realm.kotlin.types.annotations.PrimaryKey +import kotlinx.datetime.Instant + +fun LocalInAppJsonMessage.toInAppMessage(from: String, tag: String, timeStamp: Instant): InAppMessage { + return InAppMessage( + id = id, + from = from, + timestamp = timeStamp, + text = text, + tag = tag, + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = version + ) +} + +fun InAppMessage.toEntity(): InAppMessageEntity { + return InAppMessageEntity().apply { + @PrimaryKey + id = this@toEntity.id + version = this@toEntity.version + isUnRead = true + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesGraph.kt new file mode 100644 index 00000000..aa801671 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesGraph.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.messages.presentation.ui.components.MessageBottomSheetScreen +import de.gematik.ti.erp.app.messages.presentation.ui.screens.MessageDetailScreen +import de.gematik.ti.erp.app.messages.presentation.ui.screens.MessageListScreen +import de.gematik.ti.erp.app.navigation.renderBottomSheet +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideOutUp + +fun NavGraphBuilder.messagesGraph( + startDestination: String = MessagesRoutes.MessageListScreen.route, + navController: NavController +) { + navigation( + startDestination = startDestination, + route = MessagesRoutes.subGraphName() + ) { + renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, + route = MessagesRoutes.MessageListScreen.route, + arguments = MessagesRoutes.MessageListScreen.arguments + ) { navEntry -> + MessageListScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderComposable( + route = MessagesRoutes.MessageDetailScreen.route, + arguments = MessagesRoutes.MessageDetailScreen.arguments + ) { navEntry -> + MessageDetailScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = MessagesRoutes.MessageBottomSheetScreen.route, + arguments = MessagesRoutes.MessageBottomSheetScreen.arguments + ) { navEntry -> + MessageBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesRoutes.kt new file mode 100644 index 00000000..1b990ff7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/navigation/MessagesRoutes.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.navigation + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavType +import androidx.navigation.navArgument +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.navigation.fromNavigationString +import de.gematik.ti.erp.app.navigation.toNavigationString + +object MessagesRoutes : NavigationRoutes { + override fun subGraphName() = "messages" + const val MESSAGE_NAV_ORDER_ID = "MESSAGE_NAV_ORDER_ID" + const val MESSAGE_NAV_ORDER_DETAIL = "MESSAGE_NAV_ORDER_DETAIL" + const val MESSAGE_NAV_SELECTED_MESSAGE = "MESSAGE_NAV_SELECTED_MESSAGE" + const val MESSAGE_NAV_IS_LOCAL_MESSAGE = "MESSAGE_NAV_IS_LOCAL_MESSAGE" + + object MessageListScreen : Routes(NavigationRouteNames.MessageListScreen.name) + + object MessageDetailScreen : Routes( + NavigationRouteNames.MessageDetailScreen.name, + navArgument(MESSAGE_NAV_ORDER_ID) { type = NavType.StringType }, + navArgument(MESSAGE_NAV_IS_LOCAL_MESSAGE) { type = NavType.BoolType } + ) { + fun path(orderId: String, isLocalMessage: Boolean = false) = + MessageDetailScreen.path(MESSAGE_NAV_ORDER_ID to orderId, MESSAGE_NAV_IS_LOCAL_MESSAGE to isLocalMessage) + } + + object MessageBottomSheetScreen : Routes( + NavigationRouteNames.MessageBottomSheetScreen.name, + navArgument(MESSAGE_NAV_ORDER_DETAIL) { type = NavType.StringType }, + navArgument(MESSAGE_NAV_SELECTED_MESSAGE) { type = NavType.StringType } + ) { + fun path(orderDetail: OrderUseCaseData.OrderDetail, selectedMessage: OrderUseCaseData.Message) = + MessageBottomSheetScreen.path( + MESSAGE_NAV_ORDER_DETAIL to orderDetail.toNavigationString(), + MESSAGE_NAV_SELECTED_MESSAGE to selectedMessage.toNavigationString() + ) + } +} + +class MessagesRoutesBackStackEntryArguments( + private val navBackStackEntry: NavBackStackEntry +) { + internal fun orderId(): String { + return requireNotNull(navBackStackEntry.arguments?.getString(MessagesRoutes.MESSAGE_NAV_ORDER_ID)) + } + + internal fun isLocalMessage(): Boolean { + return (navBackStackEntry.arguments?.getBoolean(MessagesRoutes.MESSAGE_NAV_IS_LOCAL_MESSAGE)) ?: false + } + + internal fun getOrderDetail(): OrderUseCaseData.OrderDetail? = + navBackStackEntry.arguments?.let { bundle -> + bundle.getString(MessagesRoutes.MESSAGE_NAV_ORDER_DETAIL)?.let { + fromNavigationString(it) + } + } + + internal fun getSelectedMessage(): OrderUseCaseData.Message? = + navBackStackEntry.arguments?.let { bundle -> + bundle.getString(MessagesRoutes.MESSAGE_NAV_SELECTED_MESSAGE)?.let { + fromNavigationString(it) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageDetailController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageDetailController.kt new file mode 100644 index 00000000..f10a8bac --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageDetailController.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.domain.usecase.FetchInAppMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.FetchWelcomeMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessageUsingOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetProfileByOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetRepliedMessagesUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.SetInternalMessageAsReadUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByOrderIdAndCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateInvoicesByOrderIdAndTaskIdUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetPharmacyByTelematikIdUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class MessageDetailController( + private val orderId: String, + private val isLocalMessage: Boolean = false, + private val getRepliedMessagesUseCase: GetRepliedMessagesUseCase, + private val getMessageUsingOrderIdUseCase: GetMessageUsingOrderIdUseCase, + private val fetchInAppMessageUseCase: FetchInAppMessageUseCase, + private val fetchWelcomeMessageUseCase: FetchWelcomeMessageUseCase, + private val setInternalMessageIsReadUseCase: SetInternalMessageAsReadUseCase, + private val updateCommunicationByCommunicationIdUseCase: UpdateCommunicationByCommunicationIdUseCase, + private val updateCommunicationByOrderIdAndCommunicationIdUseCase: UpdateCommunicationByOrderIdAndCommunicationIdUseCase, + private val updateInvoicesByOrderIdAndTaskIdUseCase: UpdateInvoicesByOrderIdAndTaskIdUseCase, + private val getPharmacyByTelematikIdUseCase: GetPharmacyByTelematikIdUseCase, + private val getProfileByOrderIdUseCase: GetProfileByOrderIdUseCase +) : Controller() { + + val _localMessages = MutableStateFlow>(listOf()) + private val _messages = MutableStateFlow>>(UiState.Loading()) + private val _order = MutableStateFlow>(UiState.Loading()) + private val _pharmacy = MutableStateFlow>(UiState.Loading()) + private val _profile = MutableStateFlow(null) + + val localMessages: StateFlow> = _localMessages + val messages: StateFlow>> = _messages + val order: StateFlow> = _order + val pharmacy: StateFlow> = _pharmacy + val profile: StateFlow = _profile + + fun init() { + if (isLocalMessage) { + fetchLocalMessages() + } else { + loadOrdersAndInvoiceMessage() + loadReplyMessages() + loadProfile() + } + } + + private fun fetchLocalMessages() { + controllerScope.launch { + setInternalMessageIsReadUseCase.invoke() + combine( + fetchInAppMessageUseCase.invoke(), + fetchWelcomeMessageUseCase.invoke() + ) { localMessages, welcomeMessage -> + buildList { + addAll(localMessages) + welcomeMessage?.let { add(it) } + } + }.catch { + _localMessages.value = listOf() + }.collect { messagesList -> + _localMessages.value = messagesList.sortedByDescending { it.timestamp } + } + } + } + + private fun loadOrdersAndInvoiceMessage() { + controllerScope.launch { + val result = runCatching { + getMessageUsingOrderIdUseCase(orderId) + } + result.fold(onSuccess = { orderList -> + val order = orderList.firstOrNull() ?: run { + _order.value = UiState.Empty() + return@fold + } + + _order.value = UiState.Data(order) + getPharmacy(order.pharmacy.id) + }, onFailure = { + _order.value = UiState.Error(it) + _pharmacy.value = UiState.Error(it) + }) + } + } + + private fun loadReplyMessages() { + controllerScope.launch { + _order.collect { state -> + if (state.isDataState) { + state.data?.pharmacy?.id?.let { telematikId -> + val result = runCatching { + getRepliedMessagesUseCase(orderId, telematikId) + } + + result.fold(onSuccess = { messages -> + val messageList = messages.firstOrNull() ?: emptyList() + if (messageList.isEmpty()) { + _messages.value = UiState.Empty() + } else { + _messages.value = UiState.Data(messageList) + } + }, onFailure = { + _messages.value = UiState.Error(it) + }) + } + } + } + } + } + + fun consumeAllMessages(onMessagesConsumed: () -> Unit) { + controllerScope.launch { + // Marks the replied messages as read + _messages.value.data?.forEach { message -> + updateCommunicationByCommunicationIdUseCase(message.communicationId) + } + + // Marks the dispense request and invoice messages as read + _order.value.data?.let { orderDetail -> + updateCommunicationByOrderIdAndCommunicationIdUseCase(orderDetail.orderId) + updateInvoicesByOrderIdAndTaskIdUseCase(orderDetail.orderId) + } + + onMessagesConsumed() + } + } + + private fun getPharmacy(telematikId: String) { + controllerScope.launch { + getPharmacyByTelematikIdUseCase(telematikId).fold(onSuccess = { pharmacy -> + if (pharmacy != null) { + _pharmacy.value = UiState.Data(pharmacy) + } else { + _pharmacy.value = UiState.Empty() + } + }, onFailure = { + _pharmacy.value = UiState.Error(it) + }) + } + } + + private fun loadProfile() { + controllerScope.launch { + getProfileByOrderIdUseCase(orderId).collect { profile -> + _profile.value = profile + } + } + } +} + +@Composable +fun rememberMessageDetailController( + orderId: String, + isLocalMessage: Boolean +): MessageDetailController { + val getRepliedMessagesUseCase: GetRepliedMessagesUseCase by rememberInstance() + val getMessageUsingOrderIdUseCase: GetMessageUsingOrderIdUseCase by rememberInstance() + val fetchInAppMessageUseCase: FetchInAppMessageUseCase by rememberInstance() + val setInternalMessageAsReadUseCase: SetInternalMessageAsReadUseCase by rememberInstance() + val fetchWelcomeMessageUseCase: FetchWelcomeMessageUseCase by rememberInstance() + val updateCommunicationByCommunicationIdUseCase: UpdateCommunicationByCommunicationIdUseCase by rememberInstance() + val updateCommunicationByOrderIdAndCommunicationIdUseCase: UpdateCommunicationByOrderIdAndCommunicationIdUseCase by rememberInstance() + val updateInvoicesByOrderIdAndTaskIdUseCase: UpdateInvoicesByOrderIdAndTaskIdUseCase by rememberInstance() + val getPharmacyByTelematikIdUseCase by rememberInstance() + val getProfileByOrderIdUseCase by rememberInstance() + + return remember(orderId) { + MessageDetailController( + orderId = orderId, + isLocalMessage = isLocalMessage, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageAsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageListController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageListController.kt new file mode 100644 index 00000000..3b37010d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/MessageListController.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import de.gematik.ti.erp.app.animated.AnimationTime +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature +import de.gematik.ti.erp.app.featuretoggle.usecase.IsNewFeatureSeenUseCase +import de.gematik.ti.erp.app.featuretoggle.usecase.MarkNewFeatureSeenUseCase +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.domain.usecase.FetchInAppMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.FetchWelcomeMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessagesUseCase +import de.gematik.ti.erp.app.messages.presentation.ui.model.ViewState +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class MessageListController( + private val getMessagesUseCase: GetMessagesUseCase, + private val getProfilesUseCase: GetProfilesUseCase, + private val fetchInAppMessageUseCase: FetchInAppMessageUseCase, + private val fetchWelcomeMessageUseCase: FetchWelcomeMessageUseCase, + private val isNewFeatureSeenUseCase: IsNewFeatureSeenUseCase, + private val markNewFeatureSeenUseCase: MarkNewFeatureSeenUseCase, + private val context: Context +) : Controller() { + + private val _viewState: MutableStateFlow = MutableStateFlow(ViewState(isMessagesListFeatureChangeSeen = false, messagesList = UiState.Loading())) + private val _isMessagesListFeatureChangeSeen: MutableStateFlow = MutableStateFlow(true) + + val viewState: StateFlow = _viewState.asStateFlow() + val isMessagesListFeatureChangeSeen: StateFlow = _isMessagesListFeatureChangeSeen.asStateFlow() + + init { + fetchMessagesList() + isProfileTopBarRemovedChangeSeen() + } + + // the profile change was possible on this screen before. This change is informed to the user since release 1.24.0 + private fun isProfileTopBarRemovedChangeSeen() { + controllerScope.launch { + runCatching { + getProfilesUseCase.invoke().first() to isNewFeatureSeenUseCase.invoke(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) + }.fold(onSuccess = { (profiles, isSeen) -> + val hasMultipleProfiles = profiles.size > 1 + _viewState.value = viewState.value.copy(isMessagesListFeatureChangeSeen = if (hasMultipleProfiles) isSeen else true) + }, onFailure = { + // don't show the feature change if there is an error + _viewState.value = viewState.value.copy(isMessagesListFeatureChangeSeen = true) + }) + } + } + + fun retryFetchMessagesList() { + controllerScope.launch { + _viewState.value = viewState.value.copy(messagesList = UiState.Loading()) + delay(AnimationTime.SHORT_DELAY) + fetchMessagesList() + } + } + + private fun fetchMessagesList() { + controllerScope.launch { + _viewState.value = viewState.value.copy(messagesList = UiState.Loading()) + combine( + fetchInAppMessageUseCase.invoke(), + fetchWelcomeMessageUseCase.invoke() + ) { localMessages, welcomeMessage -> + val inAppMessages = localMessages.toMutableList() + welcomeMessage?.let { inAppMessages.add(it) } + inAppMessages.sortByDescending { it.timestamp } + combineMessages(inAppMessages.first()) + }.catch { exception -> + _viewState.value = viewState.value.copy(messagesList = UiState.Error(exception)) + }.collect { messagesList -> + if (messagesList.isEmpty()) { + _viewState.value = viewState.value.copy(messagesList = UiState.Empty()) + } else { + _viewState.value = viewState.value.copy(messagesList = UiState.Data(messagesList)) + } + } + } + } + + private suspend fun combineMessages( + localMessages: InAppMessage + ): List { + val remoteMessages = getMessagesUseCase.invoke().map { + InAppMessage( + id = it.orderId, + from = it.pharmacy.name, + timestamp = it.sentOn, + text = getMessageText(it.latestCommunicationMessage, it.pharmacy.name), + prescriptionsCount = it.prescriptions.size, + tag = "", + isUnread = it.hasUnreadMessages, + lastMessage = it.latestCommunicationMessage, + messageProfile = it.latestCommunicationMessage?.profile, + version = "" + ) + } + return buildList { + addAll(remoteMessages) + add(localMessages) + }.sortedByDescending { it.timestamp } + } + + private fun getMessageText(latestCommunicationMessage: OrderUseCaseData.LastMessage?, pharmacy: String): String { + val messageDetails: String + when (latestCommunicationMessage?.profile) { + CommunicationProfile.ErxCommunicationReply -> { + val lastMessageDetails = latestCommunicationMessage.lastMessageDetails + when { + lastMessageDetails.pickUpCodeDMC != null || lastMessageDetails.pickUpCodeHR != null -> + messageDetails = + context.getString(R.string.order_pickup_general_message) + + lastMessageDetails.link != null -> messageDetails = lastMessageDetails.message ?: context.getString(R.string.order_message_link) + + lastMessageDetails.message != null -> messageDetails = lastMessageDetails.message + + else -> messageDetails = context.getString(R.string.order_message_empty) + } + } + + CommunicationProfile.ErxCommunicationDispReq -> messageDetails = context.getString( + R.string.orders_prescription_sent_to, + latestCommunicationMessage.lastMessageDetails.message ?: pharmacy + ) + + else -> return context.getString(R.string.order_message_empty) + } + return messageDetails + } + + fun markProfileTopBarRemovedChangeSeen() { + controllerScope.launch { + markNewFeatureSeenUseCase.invoke(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) + } + } +} + +@Composable +fun rememberMessageListController(): MessageListController { + val getMessagesUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val fetchInAppMessageUseCase by rememberInstance() + val fetchWelcomeMessageUseCase by rememberInstance() + val isNewFeatureSeenUseCase by rememberInstance() + val markNewFeatureSeenUseCase by rememberInstance() + val context = LocalContext.current + return remember { + MessageListController( + getMessagesUseCase = getMessagesUseCase, + getProfilesUseCase = getProfilesUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + isNewFeatureSeenUseCase = isNewFeatureSeenUseCase, + markNewFeatureSeenUseCase = markNewFeatureSeenUseCase, + context = context + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DispenseMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DispenseMessage.kt new file mode 100644 index 00000000..ee52f501 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DispenseMessage.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.annotatedLinkUnderlined +import de.gematik.ti.erp.app.utils.extensions.DateTimeUtils +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun DispenseMessage( + pharmacyName: String, + orderSentOn: Instant, + dateFormatter: DateTimeFormatter = DateTimeUtils.dateFormatter, + timeFormatter: DateTimeFormatter = DateTimeUtils.timeFormatter, + onClickPharmacy: () -> Unit, + isLastMessage: Boolean +) { + val date = remember(orderSentOn) { + dateFormatter.format(orderSentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) + } + val time = remember(orderSentOn) { + timeFormatter.format(orderSentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) + } + + Row( + Modifier + .drawConnectedLine( + drawTop = true, + drawBottom = !isLastMessage + ) + .clickable( + onClick = onClickPharmacy + ) + ) { + Spacer(Modifier.width(SizeDefaults.sixfold)) + Column( + Modifier + .weight(1f) + .padding(PaddingDefaults.Medium) + ) { + SpacerMedium() + Text( + stringResource(R.string.orders_timestamp, date, time), + style = AppTheme.typography.subtitle2 + ) + PrescriptionSentTo(pharmacyName) + } + } +} + +@Composable +fun PrescriptionSentTo( + pharmacyName: String +) { + val fullText = when { + pharmacyName.isEmpty() -> stringResource( + R.string.orders_prescription_sent_to, + stringResource(R.string.pharmacy_order_pharmacy) + ) + + else -> stringResource(R.string.orders_prescription_sent_to, pharmacyName) + } + val annotatedText = annotatedLinkUnderlined(fullText, pharmacyName, "PharmacyNameClickable") + + Text( + text = annotatedText, + style = AppTheme.typography.body2l + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DrawConnectedLine.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DrawConnectedLine.kt new file mode 100644 index 00000000..ac05ee09 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/DrawConnectedLine.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import de.gematik.ti.erp.app.extensions.toPx +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +internal fun Modifier.drawConnectedLine( + drawTop: Boolean, + drawBottom: Boolean +) = composed { + val lineColor = AppTheme.colors.neutral300 + val circleBackground = AppTheme.colors.neutral000 + val strokeWidth = SizeDefaults.quarter.toPx() + val circleRadius = SizeDefaults.onefold.toPx() + val backgroundRadius = SizeDefaults.threeSeventyFifth.toPx() + + drawBehind { + val center = Offset(x = SizeDefaults.triple.toPx(), y = size.height / 4f) + val start = if (drawTop) Offset(x = center.x, y = 0f) else center + val end = if (drawBottom) Offset(x = center.x, y = size.height) else center + + drawLine( + color = lineColor, + strokeWidth = strokeWidth, + start = start, + end = end + ) + + drawCircle(color = lineColor, center = center, radius = circleRadius) + drawCircle(color = circleBackground, center = center, radius = backgroundRadius) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/InvoiceMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/InvoiceMessage.kt new file mode 100644 index 00000000..651329bd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/InvoiceMessage.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.extensions.DateTimeUtils +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime + +@Composable +internal fun InvoiceMessage( + prescriptionName: String?, + invoiceDate: Instant?, + isFirstMessage: Boolean, + + onClickCostReceiptDetail: () -> Unit +) { + val date = remember(invoiceDate) { + DateTimeUtils.dateFormatter.format(invoiceDate?.toLocalDateTime(TimeZone.currentSystemDefault())?.toJavaLocalDateTime()) // extention for Instant + } + val time = remember(invoiceDate) { + DateTimeUtils.timeFormatter.format(invoiceDate?.toLocalDateTime(TimeZone.currentSystemDefault())?.toJavaLocalDateTime()) // extention for Instant + } + + Row( + Modifier.drawConnectedLine( + drawTop = !isFirstMessage, + drawBottom = true + ) + ) { + Spacer(Modifier.width(SizeDefaults.sixfold)) + Column( + Modifier + .weight(1f) + .padding(PaddingDefaults.Medium) + ) { + SpacerMedium() + Text( + stringResource(R.string.orders_timestamp, date, time), + style = AppTheme.typography.subtitle2 + ) + prescriptionName?.let { + Text( + text = annotatedStringResource( + R.string.cost_receipt_is_ready, + it + ), + style = AppTheme.typography.body2l + ) + } + SpacerTiny() + CostReceiptDetail( + onClick = { onClickCostReceiptDetail() } + ) + } + } +} + +@Composable +fun CostReceiptDetail( + onClick: () -> Unit +) { + Row(modifier = Modifier.clickable(onClick = onClick)) { + Text( + text = stringResource(R.string.show_cost_receipt), + style = AppTheme.typography.body2l, + color = AppTheme.colors.primary600 + ) + Icon( + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterVertically), + imageVector = Icons.Outlined.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.primary600 + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/LocalMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/LocalMessage.kt new file mode 100644 index 00000000..c8464a4c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/LocalMessage.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.extensions.DateTimeUtils +import dev.jeziellago.compose.markdowntext.MarkdownText +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun InAppMessage( + message: OrderUseCaseData.Message, + isFirstMessage: Boolean, + isLastMessage: Boolean, + dateFormatter: DateTimeFormatter = DateTimeUtils.dateFormatter, + timeFormatter: DateTimeFormatter = DateTimeUtils.timeFormatter, + onLinkClicked: () -> Unit +) { + val localDateTime = remember(message) { + message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() + } + + val date = remember(localDateTime) { + dateFormatter.format(localDateTime) + } + + val time = remember(localDateTime) { + timeFormatter.format(localDateTime) + } + + val messageDescription = getDescriptionForMessageType(message) + + Row( + Modifier + .drawConnectedLine( + drawTop = !isFirstMessage, + drawBottom = !isLastMessage + ) + ) { + Spacer(Modifier.width(SizeDefaults.sixfold)) + Column( + Modifier + .weight(1f) + .padding(PaddingDefaults.Medium) + ) { + SpacerMedium() + Text( + stringResource(R.string.orders_timestamp, date, time), + style = AppTheme.typography.subtitle2 + ) + SpacerTiny() + if (message.additionalInfo.isNotEmpty()) { + Box( + Modifier + .clip(CircleShape) + .background(AppTheme.colors.primary100) + .padding(horizontal = PaddingDefaults.Small, vertical = SizeDefaults.threeSeventyFifth), + contentAlignment = Alignment.Center + ) { + Text( + text = message.additionalInfo, + style = AppTheme.typography.caption2 + ) + } + } + messageDescription.let { + SpacerTiny() + MarkdownText( + markdown = it, + style = AppTheme.typography.body2l, + linkColor = AppTheme.colors.primary600, + onClick = { + onLinkClicked() + } + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessageBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessageBottomSheetScreen.kt new file mode 100644 index 00000000..cf369923 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessageBottomSheetScreen.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutesBackStackEntryArguments +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessagesPreviewParameterProvider +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall +import de.gematik.ti.erp.app.utils.compose.createBitMatrix +import de.gematik.ti.erp.app.utils.compose.drawDataMatrix +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty +import de.gematik.ti.erp.app.utils.letNotNull +import de.gematik.ti.erp.app.utils.letNotNullOnCondition +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +class MessageBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = true) { + @Composable + override fun Content() { + val arguments = MessagesRoutesBackStackEntryArguments(navBackStackEntry) + + arguments.getOrderDetail()?.let { orderDetail -> + arguments.getSelectedMessage()?.let { message -> + MessageBottomSheetScreenContent( + order = orderDetail, + message = message, + onClickClose = { navController.popBackStack() } + ) + } + } ?: run { + ErrorScreenComponent() + } + } +} + +@Composable +fun MessageBottomSheetScreenContent( + order: OrderUseCaseData.OrderDetail?, + message: OrderUseCaseData.Message?, + onClickClose: () -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + .padding(bottom = PaddingDefaults.Large, top = PaddingDefaults.Medium) + .navigationBarsPadding() + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small), + horizontalArrangement = Arrangement.End + ) { + IconButton( + onClick = onClickClose, + modifier = Modifier + .padding(top = SizeDefaults.one) + ) { + Box( + Modifier + .size(SizeDefaults.fourfold) + .background(AppTheme.colors.neutral100, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Rounded.Close, contentDescription = null, tint = AppTheme.colors.neutral600) + } + } + } + SpacerMedium() + if (order == null && message == null) { + ErrorScreenComponent(noMaxSize = true) + } + + letNotNull(order, message) { order, message -> + Box( + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .testTag(TestTag.Orders.Messages.Content) + ) { + when (message.type) { + OrderUseCaseData.Message.Type.All -> AllSheetContent(message) + OrderUseCaseData.Message.Type.Link -> LinkSheetContent(message) + OrderUseCaseData.Message.Type.PickUpCodeDMC, + OrderUseCaseData.Message.Type.PickUpCodeHR -> CodeSheetContent(message) + OrderUseCaseData.Message.Type.Text -> TextSheetContent(message) + OrderUseCaseData.Message.Type.Empty -> EmptySheetContent(order.pharmacy.pharmacyName()) + } + } + } + } +} + +@Composable +private fun AllSheetContent( + message: OrderUseCaseData.Message +) { + Column(verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large)) { + CodeSheetContent(message) + message.message?.let { TextSheetContent(message) } + message.link?.let { LinkSheetContent(message) } + } +} + +@Composable +fun LinkSheetContent( + message: OrderUseCaseData.Message +) { + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Orders.Messages.Link), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (message.type != OrderUseCaseData.Message.Type.All) { + Text( + stringResource(R.string.orders_cart_ready), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + stringResource(R.string.orders_cart_ready_info), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + SpacerLarge() + } + PrimaryButtonSmall( + modifier = Modifier.testTag(TestTag.Orders.Messages.LinkButton), + onClick = { + message.link?.let { uriHandler.openUriWhenValid(it) } + } + ) { + Text(stringResource(R.string.orders_open_cart_link)) + } + } +} + +@Composable +fun TextSheetContent( + message: OrderUseCaseData.Message +) { + Column( + modifier = Modifier + .fillMaxWidth() + .semantics(true) { testTag = TestTag.Orders.Messages.Text } + ) { + Box( + Modifier + .fillMaxWidth() + .background(AppTheme.colors.neutral100, RoundedCornerShape(16.dp)) + .padding(PaddingDefaults.Medium) + ) { + Text( + message.message ?: "", + style = AppTheme.typography.body2 + ) + } + SpacerSmall() + Text( + sentOn(message), + style = AppTheme.typography.caption1l, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.End) + ) + } +} + +@Composable +private fun sentOn(message: OrderUseCaseData.Message): String = + remember(message) { + val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) + dateFormatter.format(message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) + } + +@Composable +fun CodeSheetContent( + message: OrderUseCaseData.Message +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + letNotNullOnCondition( + first = message.pickUpCodeDMC, + condition = { message.pickUpCodeDMC?.isNotBlank() == true } + ) { code -> + DataMatrixCode(payload = code, modifier = Modifier.size(SizeDefaults.eighteenfold)) + SpacerMedium() + } + + letNotNullOnCondition( + first = message.pickUpCodeHR, + condition = { message.pickUpCodeHR?.isNotBlank() == true } + ) { code -> + CodeLabel(code = code) + SpacerSmall() + } + + if (message.pickUpCodeHR.isNotNullOrEmpty() || message.pickUpCodeDMC.isNotNullOrEmpty()) { + OrdersCodeInfo() + } + } +} + +@Composable +fun OrdersCodeInfo() { + Text( + stringResource(R.string.orders_code_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + stringResource(R.string.orders_code_info), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) +} + +@Composable +private fun CodeLabel( + code: String +) { + Box( + Modifier + .background(AppTheme.colors.neutral100, RoundedCornerShape(SizeDefaults.one)) + .padding(horizontal = PaddingDefaults.ShortMedium, vertical = PaddingDefaults.ShortMedium / 2) + .semantics(true) { testTag = TestTag.Orders.Messages.CodeLabelContent } + ) { + Text( + code, + style = AppTheme.typography.subtitle2l, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } +} + +@Composable +fun EmptySheetContent(pharmacyName: String) { + Column( + Modifier + .fillMaxWidth() + .testTag(TestTag.Orders.Messages.Empty), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + stringResource(R.string.orders_no_message_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + stringResource(R.string.orders_no_message, pharmacyName), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun DataMatrixCode(payload: String, modifier: Modifier) { + val matrix = remember(payload) { createBitMatrix(payload) } + + Box( + modifier = Modifier + .then(modifier) + .background(Color.White) + .padding(PaddingDefaults.Small) + .testTag(TestTag.Orders.Messages.Code) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .drawDataMatrix(matrix) + ) + } +} + +@Composable +fun OrderUseCaseData.Pharmacy.pharmacyName() = + name.ifBlank { + stringResource(R.string.orders_generic_pharmacy_name) + } + +@LightDarkPreview +@Composable +fun MessageBottomSheetScreenContentNoOrderEmptyPreview() { + PreviewAppTheme { + MessageBottomSheetScreenContent( + order = null, + message = null, + onClickClose = {} + ) + } +} + +@LightDarkPreview +@Composable +fun MessageBottomSheetScreenContentPreview( + @PreviewParameter(MessagesPreviewParameterProvider::class) message: OrderUseCaseData.Message? +) { + PreviewAppTheme { + MessageBottomSheetScreenContent( + order = MessageSheetsPreviewData.ORDER_DETAIL_PREVIEW, + message = message, + onClickClose = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Messages.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Messages.kt new file mode 100644 index 00000000..af71401c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Messages.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.presentation.ui.model.MessageDetailCombinedMessage +import de.gematik.ti.erp.app.messages.presentation.ui.model.MessageType.DISPENSE +import de.gematik.ti.erp.app.messages.presentation.ui.model.MessageType.IN_APP +import de.gematik.ti.erp.app.messages.presentation.ui.model.MessageType.INVOICE +import de.gematik.ti.erp.app.messages.presentation.ui.model.MessageType.REPLY +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.prescriptionId +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun Messages( + modifier: Modifier = Modifier, + listState: LazyListState, + order: UiState, + messages: List, + inAppMessages: List, + onClickReplyMessage: (OrderUseCaseData.Message) -> Unit, + onClickPrescription: (String) -> Unit, + onClickInvoiceMessage: (String) -> Unit, + onClickPharmacy: () -> Unit +) { + val combinedMessages = remember(messages, order, inAppMessages) { + combineMessagesAndSort(messages, order, inAppMessages) + } + LazyColumn( + modifier = Modifier + .testTag(TestTag.Orders.Details.Content) + .then(modifier), + state = listState, + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) + .asPaddingValues() + ) { + item { + SpacerMedium() + Text( + stringResource(R.string.messages_history_title), + style = AppTheme.typography.h6, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) + } + + combinedMessages.forEachIndexed { index, displayMessage -> + val isFirstMessage = index == 0 + val isLastMessage = index == combinedMessages.size - 1 + + when (displayMessage.type) { + REPLY -> { + displayMessage.message?.let { message -> + item { + ReplyMessage( + message = message, + isFirstMessage = isFirstMessage, + isLastMessage = isLastMessage, + onClick = { onClickReplyMessage(message) } + ) + } + } + } + + INVOICE -> { + item { + InvoiceMessage( + prescriptionName = displayMessage.orderDetail?.taskDetailedBundles?.firstOrNull()?.prescription?.name ?: "", + invoiceDate = displayMessage.orderDetail?.taskDetailedBundles?.firstOrNull()?.invoiceInfo?.invoiceSentOn, + isFirstMessage = isFirstMessage, + onClickCostReceiptDetail = { + displayMessage.orderDetail?.taskDetailedBundles?.firstOrNull()?.prescription?.taskId?.let { + onClickInvoiceMessage(it) + } + } + ) + } + } + + DISPENSE -> { + item { + DispenseMessage( + pharmacyName = displayMessage.orderDetail?.pharmacy?.name ?: "", + orderSentOn = displayMessage.timestamp, + isLastMessage = isLastMessage, + onClickPharmacy = onClickPharmacy + ) + } + } + + IN_APP -> { + displayMessage.message?.let { message -> + item { + InAppMessage( + message = message, + isFirstMessage = isFirstMessage, + isLastMessage = isLastMessage, + onLinkClicked = {} + ) + } + } + } + } + } + if (combinedMessages.isNotEmpty() && combinedMessages[0].type != IN_APP) { + item { + SpacerXXLarge() + Divider(color = AppTheme.colors.neutral300) + SpacerXXLarge() + Text( + text = stringResource(R.string.messages_cart_title), + style = AppTheme.typography.h6, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) + } + item { + UiStateMachine( + state = order, + onContent = { orderDetail -> + Column( + Modifier.padding(PaddingDefaults.Medium), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + orderDetail.taskDetailedBundles.forEachIndexed { index, taskBundle -> + Surface( + modifier = Modifier + .testTag(TestTag.Orders.Details.PrescriptionListItem) + .semantics { + prescriptionId = orderDetail.taskDetailedBundles[index].prescription?.taskId + }, + shape = RoundedCornerShape(SizeDefaults.one), + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral300), + color = AppTheme.colors.neutral050, + onClick = { + orderDetail.taskDetailedBundles[index].prescription?.taskId?.let { taskId -> + onClickPrescription(taskId) + } + } + ) { + Row( + Modifier.padding(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + val name = when (taskBundle.prescription) { + is Prescription.ScannedPrescription -> taskBundle.prescription.name + is Prescription.SyncedPrescription -> taskBundle.prescription.name ?: "" + else -> "" + } + Text( + name, + style = AppTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + SpacerMedium() + Icon( + Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.neutral400 + ) + } + } + } + } + } + ) + } + } + } +} + +fun combineMessagesAndSort( + messages: List, + order: UiState, + inAppMessages: List +): List { + val messageItems = messages.map { + MessageDetailCombinedMessage( + type = REPLY, + message = it, + timestamp = it.sentOn + ) + } + + val invoiceItems = order.data?.taskDetailedBundles + ?.filter { it.invoiceInfo.hasInvoice } + ?.mapNotNull { taskBundle -> + taskBundle.invoiceInfo.invoiceSentOn?.let { + MessageDetailCombinedMessage( + type = INVOICE, + orderDetail = order.data, + timestamp = it + ) + } + }.orEmpty() + + val dispenseItem = order.data?.let { orderDetail -> + MessageDetailCombinedMessage( + type = DISPENSE, + orderDetail = orderDetail, + timestamp = orderDetail.sentOn + ) + } + + val inAppMessageItems = inAppMessages.map { local -> + MessageDetailCombinedMessage( + type = IN_APP, + message = OrderUseCaseData.Message( + message = local?.text.orEmpty(), + additionalInfo = local?.tag.orEmpty(), + sentOn = Instant.parse(local?.timestamp.toString()), + communicationId = local?.id.orEmpty(), + link = null, + consumed = true, + pickUpCodeDMC = null, + pickUpCodeHR = null + ), + timestamp = Instant.parse(local?.timestamp.toString()) + ) + } + + return (inAppMessageItems + messageItems + invoiceItems + listOfNotNull(dispenseItem)).sortedByDescending { it.timestamp } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessagesLoadingShimmer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessagesLoadingShimmer.kt new file mode 100644 index 00000000..91be41a2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/MessagesLoadingShimmer.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.valentinilk.shimmer.shimmer +import de.gematik.ti.erp.app.shimmer.MessageOverviewItemShimmer +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun MessagesLoadingShimmer() { + Column( + modifier = Modifier + .fillMaxSize() + .shimmer() + ) { + SpacerLarge() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + MessageOverviewItemShimmer() + } +} + +@LightDarkPreview +@Composable +fun MessagesLoadingPreview() { + PreviewAppTheme { + MessagesLoadingShimmer() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/NoOrders.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/NoOrders.kt new file mode 100644 index 00000000..08e723f3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/NoOrders.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.EmptyScreenComponent + +@Composable +internal fun NoOrders( + modifier: Modifier = Modifier, + onClickRefresh: () -> Unit +) = + EmptyScreenComponent( + modifier = modifier, + title = stringResource(R.string.messages_empty_title), + body = stringResource(R.string.messages_empty_subtitle), + image = { + Image( + painterResource(R.drawable.woman_red_shirt_circle_blue), + contentDescription = null, + modifier = Modifier.size(SizeDefaults.twentyfold) + ) + }, + button = { + TextButton( + onClick = onClickRefresh + ) { + Icon( + Icons.Rounded.Refresh, + null, + modifier = Modifier.size(SizeDefaults.double), + tint = AppTheme.colors.primary600 + ) + SpacerSmall() + Text(text = stringResource(R.string.home_egk_redeemed_buttontext), textAlign = TextAlign.Right) + } + } + ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Orders.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Orders.kt new file mode 100644 index 00000000..83a61657 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/Orders.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.animated.AnimatedComponent +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.labels.InfoLabel +import de.gematik.ti.erp.app.labels.InfoLabelInBox +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXXLarge +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.timeDescription +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState + +@Composable +internal fun Orders( + modifier: Modifier = Modifier, + listState: LazyListState, + showOrderFeatureChangedLabel: Boolean, + ordersData: UiState>, + onClickInfoLabel: () -> Unit, + onClickOrder: (orderId: String, isLoacalMessage: Boolean) -> Unit, + onClickRetry: () -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + UiStateMachine( + state = ordersData, + onEmpty = { + Center { + NoOrders { onClickRetry() } + } + }, + onLoading = { + Center { + MessagesLoadingShimmer() + } + }, + onError = { + ErrorScreenComponent( + onClickRetry = onClickRetry + ) + } + ) { orders -> + LazyColumn( + modifier = modifier.testTag(TestTag.Orders.Content), + state = listState + ) { + if (showOrderFeatureChangedLabel) { + item { + InfoLabel( + modifier = Modifier + .padding(top = PaddingDefaults.Medium) + .padding(horizontal = PaddingDefaults.Medium), + text = stringResource(R.string.orders_top_app_bar_change_text), + onClose = onClickInfoLabel + ) + } + } + orders.forEachIndexed { index, order -> + item { + val timeDescription by timeDescription(instant = order.timestamp) + Order( + pharmacy = order.from, + time = timeDescription, + hasUnreadMessages = order.isUnread, + prescriptionsCount = order.prescriptionsCount, + text = order.text ?: "", + onClick = { + onClickOrder(order.id, order.messageProfile == CommunicationProfile.InApp) + } + ) + if (index < orders.size - 1) { + Divider( + Modifier.padding(start = PaddingDefaults.Medium) + ) + } + } + } + item { + SpacerXXXLarge() + } + } + } + if (showOrderFeatureChangedLabel && (ordersData.isEmptyState)) { + AnimatedComponent { + InfoLabelInBox( + text = stringResource(R.string.orders_top_app_bar_change_text), + onClose = onClickInfoLabel + ) + } + } + } +} + +@Composable +private fun Order( + modifier: Modifier = Modifier, + pharmacy: String, + time: String?, + text: String, + hasUnreadMessages: Boolean, + prescriptionsCount: Int, + onClick: () -> Unit +) { + Row( + modifier = modifier + .clickable(onClick = onClick) + .padding(PaddingDefaults.Medium) + .fillMaxWidth() + .testTag(TestTag.Orders.OrderListItem), + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + verticalAlignment = Alignment.CenterVertically + ) { + Column(Modifier.weight(1f)) { + Text( + text = pharmacy, + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral900, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerTiny() + Text( + text = text, + style = AppTheme.typography.subtitle2l, + color = AppTheme.colors.neutral900, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerTiny() + time?.let { + Text( + text = it, + style = AppTheme.typography.body2l, + color = AppTheme.colors.neutral600 + ) + } + } + when { + hasUnreadMessages -> NewMessageLabel() + else -> if (prescriptionsCount != 0) { + PrescriptionCountLabel(prescriptionsCount) + } + } + + Icon( + imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.neutral400 + ) + } +} + +@Composable +private fun NewMessageLabel() { + Box( + Modifier + .clip(CircleShape) + .background(AppTheme.colors.primary100) + .padding(horizontal = PaddingDefaults.Small, vertical = SizeDefaults.threeSeventyFifth), + contentAlignment = Alignment.Center + ) { + Text( + stringResource(R.string.orders_label_new), + style = AppTheme.typography.caption2, + color = AppTheme.colors.primary900 + ) + } +} + +@Composable +private fun PrescriptionCountLabel(count: Int) { + Box( + Modifier + .clip(CircleShape) + .background(AppTheme.colors.neutral100) + .padding(horizontal = PaddingDefaults.Small, vertical = SizeDefaults.threeSeventyFifth), + contentAlignment = Alignment.Center + ) { + Text( + annotatedPluralsResource( + R.plurals.orders_plurals_label_nr_of_prescriptions, + count, + AnnotatedString(count.toString()) + ), + style = AppTheme.typography.caption2, + color = AppTheme.colors.neutral600 + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessage.kt new file mode 100644 index 00000000..d9094c63 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessage.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.em +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.DynamicText +import de.gematik.ti.erp.app.utils.extensions.DateTimeUtils +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun ReplyMessage( + message: OrderUseCaseData.Message, + isFirstMessage: Boolean, + isLastMessage: Boolean, + dateFormatter: DateTimeFormatter = DateTimeUtils.dateFormatter, + timeFormatter: DateTimeFormatter = DateTimeUtils.timeFormatter, + onClick: () -> Unit +) { + val localDateTime = remember(message) { + message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() + } + + val date = remember(localDateTime) { + dateFormatter.format(localDateTime) + } + + val time = remember(localDateTime) { + timeFormatter.format(localDateTime) + } + + val replyMessageTitle = getTitleForMessageType(message) + val replyMessageDescription = getDescriptionForMessageType(message) + + Row( + Modifier + .drawConnectedLine( + drawTop = !isFirstMessage, + drawBottom = !isLastMessage + ) + .clickable( + onClick = onClick, + enabled = message.type != OrderUseCaseData.Message.Type.Text + ) + ) { + Spacer(Modifier.width(SizeDefaults.sixfold)) + Column( + Modifier + .weight(1f) + .padding(PaddingDefaults.Medium) + ) { + SpacerMedium() + Text( + stringResource(R.string.orders_timestamp, date, time), + style = AppTheme.typography.subtitle2 + ) + SpacerTiny() + Text( + text = replyMessageDescription, + style = AppTheme.typography.body2l + ) + replyMessageTitle?.let { + SpacerTiny() + AnnotatedInfoText(info = it) + } + } + } +} + +@Composable +private fun AnnotatedInfoText(info: String) { + val annotatedText = buildAnnotatedString { + append(info) + append(" ") + appendInlineContent("button", "button") + } + + val inlineContent = mapOf( + "button" to InlineTextContent( + Placeholder( + width = 0.em, + height = 0.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + ) { + Icon( + Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.primary600 + ) + } + ) + + DynamicText( + text = annotatedText, + style = AppTheme.typography.body2, + color = AppTheme.colors.primary600, + inlineContent = inlineContent + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessageTypeUtils.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessageTypeUtils.kt new file mode 100644 index 00000000..08b74aa9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/components/ReplyMessageTypeUtils.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData + +@Composable +fun getTitleForMessageType(message: OrderUseCaseData.Message): String? { + return handleMessageType( + message = message, + onPickupCode = { stringResource(R.string.orders_show_code) }, + onLinkWithMessage = { stringResource(R.string.orders_show_cart) }, + onLinkOnly = { stringResource(R.string.orders_show_cart) }, + onTextOnly = { null }, + onEmpty = { null } + ) +} + +@Composable +fun getDescriptionForMessageType(message: OrderUseCaseData.Message): String { + return handleMessageType( + message = message, + onPickupCode = { stringResource(R.string.order_pickup_general_message) }, + onLinkWithMessage = { message.message.orEmpty() }, + onLinkOnly = { stringResource(R.string.order_message_link) }, + onTextOnly = { message.message.orEmpty() }, + onEmpty = { stringResource(R.string.order_message_empty) } + ) +} + +@Composable +fun handleMessageType( + message: OrderUseCaseData.Message, + onPickupCode: @Composable () -> T, + onLinkWithMessage: @Composable () -> T, + onLinkOnly: @Composable () -> T, + onTextOnly: () -> T, + onEmpty: @Composable () -> T +): T { + return when { + message.pickUpCodeDMC != null || message.pickUpCodeHR != null -> onPickupCode() + message.link != null && message.message != null -> onLinkWithMessage() + message.link != null -> onLinkOnly() + message.message != null -> onTextOnly() + else -> onEmpty() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/MessageDetailCombinedMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/MessageDetailCombinedMessage.kt new file mode 100644 index 00000000..1d8bbada --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/MessageDetailCombinedMessage.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.model + +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import kotlinx.datetime.Instant + +data class MessageDetailCombinedMessage( + val type: MessageType, + val message: OrderUseCaseData.Message? = null, + val orderDetail: OrderUseCaseData.OrderDetail? = null, + val timestamp: Instant +) + +enum class MessageType { + REPLY, + INVOICE, + DISPENSE, + IN_APP +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/TrackedEvent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/TrackedEvent.kt new file mode 100644 index 00000000..7a0caa15 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/TrackedEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.model + +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareEventMapper + +sealed class TrackedEvent( + val key: ContentSquareEventMapper, + val value: String +) { + class ArchivePrescriptionCount(val count: Int) : TrackedEvent(ContentSquareEventMapper.ArchivePrescriptionCount, "$count") + class SyncedPrescriptionCount(val count: Int) : TrackedEvent(ContentSquareEventMapper.SyncedPrescriptionCount, "$count") + class ScannedPrescriptionCount(val count: Int) : TrackedEvent(ContentSquareEventMapper.ScannedPrescriptionCount, "$count") + class MessageCount(val count: Int) : TrackedEvent(ContentSquareEventMapper.MessageCount, "$count") +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/ViewState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/ViewState.kt new file mode 100644 index 00000000..9f6194e9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/model/ViewState.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.model + +import androidx.compose.runtime.Immutable +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.utils.uistate.UiState + +@Immutable +data class ViewState( + val isMessagesListFeatureChangeSeen: Boolean = false, + val messagesList: UiState> = UiState.Loading() +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailInAppPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailInAppPreviewParameterProvider.kt new file mode 100644 index 00000000..3d2e4ab0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailInAppPreviewParameterProvider.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.datetime.Instant + +class MessageDetailInAppPreviewParameterProvider : PreviewParameterProvider> { + override val values = sequenceOf( + inAppPreview + ) +} + +const val IN_APP_MESSAGE = + "Nutzen Sie ab sofort die E-Rezept App, um Ihre Entscheidung zur Organspende im **digitalen Organspenderegister** festzuhalten – sicher und " + + "#unkompliziert.\u2028\n\nAktuell warten 9.192 Menschen in Deutschland dringend auf ein Organ – jede Entscheidung zählt und kann Leben retten.\n\n" + +private val inAppPreview = listOf( + InAppMessage( + id = "123", + from = "Team", + text = IN_APP_MESSAGE, + timestamp = Instant.parse("2024-11-08T15:20:00Z"), + prescriptionsCount = 0, + tag = "Version 1.26.0", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = "1.26.0" + ), + InAppMessage( + id = "123", + from = "Team", + text = IN_APP_MESSAGE, + timestamp = Instant.parse("2024-10-08T15:20:00Z"), + prescriptionsCount = 0, + tag = "Version 1.27.0", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = "1.27.0" + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailPreviewParameterProvider.kt new file mode 100644 index 00000000..8b488870 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageDetailPreviewParameterProvider.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.EMPTY_MESSAGE_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_DMC_CODE_MESSAGE_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_DMC_HR_CODE_MESSAGE_NO_MSG_PAYLOAD_WITH_LINK_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_DMC_HR_CODE_MESSAGE_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_WITH_LINK_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.PICK_HR_CODE_MESSAGE_PREVIEW +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.orderMessageWithEmptyMessage +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.orderMessageWithOnlyMessage +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.orderMessageWithOnlyURL +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageSheetsPreviewData.orderMessageWithPickUpCode +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant + +data class OrderMessageDetail( + val orderDetail: OrderUseCaseData.OrderDetail, + val message: OrderUseCaseData.Message +) + +class MessagesPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + EMPTY_MESSAGE_PREVIEW, + PICK_DMC_CODE_MESSAGE_PREVIEW, + PICK_HR_CODE_MESSAGE_PREVIEW, + PICK_DMC_HR_CODE_MESSAGE_PREVIEW, + PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_PREVIEW, + PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_WITH_LINK_PREVIEW, + PICK_DMC_HR_CODE_MESSAGE_NO_MSG_PAYLOAD_WITH_LINK_PREVIEW + ) +} + +class MessageOrderDetailPreviewParameterProvider : PreviewParameterProvider>> { + + override val values: Sequence>> = sequenceOf( + UiState.Loading(), + UiState.Empty(), + UiState.Error(Throwable("Error")), + UiState.Data( + listOf( + orderMessageWithPickUpCode, + orderMessageWithOnlyURL, + orderMessageWithOnlyMessage, + orderMessageWithEmptyMessage + ) + ) + ) +} + +object MessageSheetsPreviewData { + + private val time: Instant = Instant.parse("2023-06-14T10:15:30Z") + + val ORDER_DETAIL_PREVIEW = OrderUseCaseData.OrderDetail( + orderId = "123", + taskDetailedBundles = listOf( + OrderUseCaseData.TaskDetailedBundle( + invoiceInfo = OrderUseCaseData.InvoiceInfo( + hasInvoice = false, + invoiceSentOn = time + ), + prescription = Prescription.ScannedPrescription( + taskId = "123", + name = "Prescription", + redeemedOn = time, + scannedOn = time, + index = 1, + communications = emptyList() + ) + ) + ), + sentOn = time, + pharmacy = OrderUseCaseData.Pharmacy("123", "Pharmacy from OrderDetail"), + hasUnreadMessages = false + ) + + val PICK_HR_CODE_MESSAGE_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = "pickUpCodeHR", + link = null, + consumed = false + ) + + val PICK_DMC_HR_CODE_MESSAGE_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = "pickUpCodeDMC", + pickUpCodeHR = "pickUpCodeHR", + link = null, + consumed = false + ) + + val PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = "message from the pharmacy!", + pickUpCodeDMC = "pickUpCodeDMC", + pickUpCodeHR = "pickUpCodeHR", + link = null, + consumed = false + ) + + val PICK_DMC_HR_CODE_MESSAGE_WITH_MSG_PAYLOAD_WITH_LINK_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = "message from the pharmacy!", + pickUpCodeDMC = "pickUpCodeDMC", + pickUpCodeHR = "pickUpCodeHR", + link = null, + consumed = false + ) + + val PICK_DMC_HR_CODE_MESSAGE_NO_MSG_PAYLOAD_WITH_LINK_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = "pickUpCodeDMC", + pickUpCodeHR = "pickUpCodeHR", + link = null, + consumed = false + ) + + val PICK_DMC_CODE_MESSAGE_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = "pickUpCodeDMC", + pickUpCodeHR = null, + link = "https://this.is.a.link.de", + consumed = false + ) + + val MESSAGE_WITH_URL_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = "https://this.is.a.link.de", + consumed = false + ) + + val MESSAGE_WITH_Only_Message_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = "message from the pharmacy!", + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = false + ) + + val EMPTY_MESSAGE_PREVIEW = OrderUseCaseData.Message( + communicationId = "123", + sentOn = time, + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = false + ) + + val orderMessageWithPickUpCode = + OrderMessageDetail( + orderDetail = ORDER_DETAIL_PREVIEW, + message = PICK_DMC_CODE_MESSAGE_PREVIEW + + ) + + val orderMessageWithOnlyURL = + OrderMessageDetail( + orderDetail = ORDER_DETAIL_PREVIEW, + message = MESSAGE_WITH_URL_PREVIEW + + ) + + val orderMessageWithOnlyMessage = + OrderMessageDetail( + orderDetail = ORDER_DETAIL_PREVIEW, + message = MESSAGE_WITH_Only_Message_PREVIEW + ) + + val orderMessageWithEmptyMessage = + OrderMessageDetail( + orderDetail = ORDER_DETAIL_PREVIEW, + message = EMPTY_MESSAGE_PREVIEW + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageListPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageListPreviewParameterProvider.kt new file mode 100644 index 00000000..fad7a96a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/MessageListPreviewParameterProvider.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant + +class MessageListParameterProvider : PreviewParameterProvider>> { + override val values: Sequence>> + get() = sequenceOf( + UiState.Loading(), + UiState.Empty(), + UiState.Error(Throwable("Error")), + UiState.Data( + listOf( + PreviewListView1, + PreviewListView2, + PreviewListView3, + PreviewListView4, + PreviewListView5, + PreviewListView6, + PreviewListView7 + ) + ) + ) +} + +const val LATEST_MESSAGE = "order message here!" +const val LATEST_MESSAGE_ADDRESS = "Berlinder Strasse 123, 12345 Berlin" +const val LATEST_MESSAGE_LONG = "This is a long message to see how it looks like when the message is long and how the UI should handle it properly." + +val mockMessageDetails = OrderUseCaseData.LastMessageDetails( + message = LATEST_MESSAGE, + pickUpCodeDMC = "DMC1234", + pickUpCodeHR = "HR5678", + link = "http://pharmacy.com/pickup" +) + +val mockMessageDetailsWithoutCode = OrderUseCaseData.LastMessageDetails( + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = "http://pharmacy.com/pickup" +) + +val mockMessageDetailsWithoutCodeAndLink = OrderUseCaseData.LastMessageDetails( + message = LATEST_MESSAGE, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null +) + +val mockMessageDetailsAddress = OrderUseCaseData.LastMessageDetails( + message = LATEST_MESSAGE_ADDRESS, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null +) +val mockLastMessageWithoutCodeAndLink = OrderUseCaseData.LastMessage( + lastMessageDetails = mockMessageDetailsWithoutCodeAndLink, + profile = CommunicationProfile.ErxCommunicationReply +) + +val mockLastMessageWithoutCode = OrderUseCaseData.LastMessage( + lastMessageDetails = mockMessageDetailsWithoutCode, + profile = CommunicationProfile.ErxCommunicationReply +) + +val mockLastMessage = OrderUseCaseData.LastMessage( + lastMessageDetails = mockMessageDetails, + profile = CommunicationProfile.ErxCommunicationReply +) + +val mockLastMessageAdress = OrderUseCaseData.LastMessage( + lastMessageDetails = mockMessageDetailsAddress, + profile = CommunicationProfile.ErxCommunicationReply +) +val mockLastMessageLong = OrderUseCaseData.LastMessage( + lastMessageDetails = mockMessageDetails.copy(message = LATEST_MESSAGE_LONG), + profile = CommunicationProfile.ErxCommunicationReply + +) + +private val PreviewListView1 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 3, + tag = "tag", + isUnread = false, + lastMessage = mockLastMessage, + messageProfile = CommunicationProfile.ErxCommunicationReply, + version = "1.0.0" +) + +private val PreviewListView2 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "tag", + isUnread = true, + lastMessage = mockLastMessageAdress, + messageProfile = CommunicationProfile.ErxCommunicationDispReq, + version = "1.0.0" +) + +private val PreviewListView3 = InAppMessage( + id = "1", + from = "Team", + text = LATEST_MESSAGE_LONG, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "Team", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = "1.0.0" +) + +private val PreviewListView4 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "tag", + isUnread = true, + lastMessage = mockLastMessageWithoutCode, + messageProfile = CommunicationProfile.ErxCommunicationReply, + version = "1.0.0" +) + +private val PreviewListView5 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "tag", + isUnread = true, + lastMessage = mockLastMessageWithoutCodeAndLink, + messageProfile = CommunicationProfile.ErxCommunicationReply, + version = "1.0.0" +) + +private val PreviewListView6 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "tag", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.ErxCommunicationReply, + version = "1.0.0" +) + +private val PreviewListView7 = InAppMessage( + id = "1", + from = "Apotheke", + text = LATEST_MESSAGE, + timestamp = Instant.parse("2023-07-08T15:20:00Z"), + prescriptionsCount = 1, + tag = "tag", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.ErxCommunicationDispReq, + version = "1.0.0" +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/OrdersPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/OrdersPreviewParameterProvider.kt new file mode 100644 index 00000000..a14c73a2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/preview/OrdersPreviewParameterProvider.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.preview + +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import kotlinx.datetime.Instant + +private val PREVIEW_PRESCRIPTION = Prescription.ScannedPrescription( + taskId = "123", + name = "Prescription", + redeemedOn = Instant.parse("2023-07-08T15:20:00Z"), + scannedOn = Instant.parse("2023-07-08T15:20:00Z"), + index = 0, + communications = emptyList() +) + +private val PREVIEW_ORDER_1 = OrderUseCaseData.Order( + orderId = "2", + sentOn = Instant.parse("2023-07-03T15:20:00Z"), + hasUnreadMessages = true, + pharmacy = OrderUseCaseData.Pharmacy("123", "Apotheke"), + prescriptions = emptyList(), + latestCommunicationMessage = mockLastMessage +) + +private val PREVIEW_ORDER_2 = OrderUseCaseData.Order( + orderId = "2", + sentOn = Instant.parse("2023-07-04T15:20:00Z"), + hasUnreadMessages = true, + pharmacy = OrderUseCaseData.Pharmacy("123", "Flughafen"), + prescriptions = emptyList(), + latestCommunicationMessage = mockLastMessageLong +) + +private val PREVIEW_ORDER_3 = OrderUseCaseData.Order( + orderId = "3", + sentOn = Instant.parse("2023-07-05T15:20:00Z"), + hasUnreadMessages = false, + pharmacy = OrderUseCaseData.Pharmacy("123", "TaxiStand"), + prescriptions = listOf(PREVIEW_PRESCRIPTION), + latestCommunicationMessage = null +) + +private val PREVIEW_ORDER_4 = OrderUseCaseData.Order( + orderId = "4", + sentOn = Instant.parse("2023-07-06T15:20:00Z"), + hasUnreadMessages = true, + pharmacy = OrderUseCaseData.Pharmacy("123", "HauptBahnhof"), + prescriptions = listOf(PREVIEW_PRESCRIPTION), + latestCommunicationMessage = mockLastMessage +) + +private val PREVIEW_ORDER_5 = OrderUseCaseData.Order( + orderId = "5", + sentOn = Instant.parse("2023-07-08T15:20:00Z"), + hasUnreadMessages = false, + pharmacy = OrderUseCaseData.Pharmacy("123", "BusStation"), + prescriptions = listOf( + PREVIEW_PRESCRIPTION, + PREVIEW_PRESCRIPTION.copy(index = 1), + PREVIEW_PRESCRIPTION.copy(index = 2) + ), + latestCommunicationMessage = mockLastMessageLong +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageDetailScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageDetailScreen.kt new file mode 100644 index 00000000..67dbbdfe --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageDetailScreen.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutes +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutesBackStackEntryArguments +import de.gematik.ti.erp.app.messages.presentation.rememberMessageDetailController +import de.gematik.ti.erp.app.messages.presentation.ui.components.Messages +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageDetailInAppPreviewParameterProvider +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageOrderDetailPreviewParameterProvider +import de.gematik.ti.erp.app.messages.presentation.ui.preview.OrderMessageDetail +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.letNotNull +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.github.aakira.napier.Napier + +class MessageDetailScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + + val messageController = rememberMessageDetailController( + orderId = MessagesRoutesBackStackEntryArguments(navBackStackEntry).orderId(), + + isLocalMessage = MessagesRoutesBackStackEntryArguments(navBackStackEntry).isLocalMessage() + ) + + LaunchedEffect(Unit) { + messageController.init() + } + var selectedMessage by remember { mutableStateOf(null) } + + val order by messageController.order.collectAsStateWithLifecycle() + val messages by messageController.messages.collectAsStateWithLifecycle() + val profileData by messageController.profile.collectAsStateWithLifecycle() + Napier.d("MessageDetailScreen: profileData: ${profileData?.id}") + val pharmacyState by messageController.pharmacy.collectAsStateWithLifecycle() + val inAppMessages by messageController.localMessages.collectAsStateWithLifecycle() + + MessageDetailScreenScaffold( + listState = listState, + onBack = { + messageController.consumeAllMessages { navController.popBackStack() } + }, + onClickReplyMessage = { message -> + selectedMessage = message + letNotNull(order.data, selectedMessage) { orderDetail, selectedMessage -> + navController.navigate( + MessagesRoutes.MessageBottomSheetScreen.path( + orderDetail = orderDetail, + selectedMessage = selectedMessage + ) + ) + } + }, + onClickPrescription = { taskId -> + navController.navigate( + PrescriptionDetailRoutes.PrescriptionDetailScreen.path(taskId = taskId) + ) + }, + onClickInvoiceMessage = { taskId -> + profileData?.let { profile -> + navController.navigate( + PkvRoutes.InvoiceDetailsScreen + .path( + taskId = taskId, + profileId = profile.id + ) + ) + } + }, + onClickPharmacy = { + pharmacyState.data?.let { pharmacy -> + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromMessageScreen.path( + pharmacy = pharmacy, + taskId = pharmacy.telematikId + ) + ) + } + }, + order = order, + messages = messages.data ?: emptyList(), + inAppMessage = inAppMessages + ) + + BackHandler { + messageController.consumeAllMessages { navController.popBackStack() } + } + } +} + +@Composable +fun MessageDetailScreenScaffold( + listState: LazyListState, + onBack: () -> Unit, + onClickReplyMessage: (OrderUseCaseData.Message) -> Unit, + onClickPrescription: (String) -> Unit, + onClickInvoiceMessage: (String) -> Unit, + onClickPharmacy: () -> Unit, + order: UiState, + messages: List, + inAppMessage: List +) { + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Orders.Details.Screen), + topBarTitle = order.data?.pharmacy?.name ?: stringResource(R.string.messages_title), + listState = listState, + navigationMode = NavigationBarMode.Back, + onBack = onBack, + topBarPadding = PaddingValues(end = PaddingDefaults.Medium) + ) { + Messages( + listState = listState, + order = order, + messages = messages, + inAppMessages = inAppMessage, + onClickReplyMessage = onClickReplyMessage, + onClickPrescription = onClickPrescription, + onClickInvoiceMessage = onClickInvoiceMessage, + onClickPharmacy = onClickPharmacy + ) + } +} + +@LightDarkPreview +@Composable +fun MessageDetailScreenWithPharmacyPreview( + @PreviewParameter(MessageOrderDetailPreviewParameterProvider::class) + orderMessageDetail: UiState> +) { + PreviewAppTheme { + MessageDetailScreenScaffold( + listState = rememberLazyListState(), + onBack = {}, + onClickReplyMessage = {}, + onClickPrescription = {}, + onClickInvoiceMessage = {}, + onClickPharmacy = {}, + order = UiState.Data(orderMessageDetail.data?.first()?.orderDetail), + messages = orderMessageDetail.data?.map { it.message } ?: emptyList(), + inAppMessage = emptyList() + ) + } +} + +@LightDarkPreview +@Composable +fun MessageDetailScreenInAppWithPharmacyPreview( + @PreviewParameter(MessageDetailInAppPreviewParameterProvider::class) + inAppMessage: List +) { + PreviewAppTheme { + MessageDetailScreenScaffold( + listState = rememberLazyListState(), + onBack = {}, + onClickReplyMessage = {}, + onClickPrescription = {}, + onClickInvoiceMessage = {}, + onClickPharmacy = {}, + order = UiState.Loading(), + messages = emptyList(), + inAppMessage = inAppMessage + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageListScreen.kt new file mode 100644 index 00000000..9031f733 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/messages/presentation/ui/screens/MessageListScreen.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.presentation.ui.screens + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.navigation.MessagesRoutes +import de.gematik.ti.erp.app.messages.presentation.rememberMessageListController +import de.gematik.ti.erp.app.messages.presentation.ui.components.Orders +import de.gematik.ti.erp.app.messages.presentation.ui.model.ViewState +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageListParameterProvider +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState + +class MessageListScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val messagesController = rememberMessageListController() + val listState = rememberLazyListState() + val viewState by messagesController.viewState.collectAsStateWithLifecycle() + var showOrderFeatureChangedLabel by remember(viewState.isMessagesListFeatureChangeSeen) { mutableStateOf(!viewState.isMessagesListFeatureChangeSeen) } + + MessageListScreenContent( + viewState = viewState, + listState = listState, + showOrderFeatureChangedLabel = showOrderFeatureChangedLabel, // a feature change info label shown to the user + onClickRetry = messagesController::retryFetchMessagesList, + onClickInfoLabel = { + showOrderFeatureChangedLabel = false + messagesController.markProfileTopBarRemovedChangeSeen() + }, + onClickOrder = { orderId, isLocalMessage -> + navController.navigate( + MessagesRoutes.MessageDetailScreen.path(orderId, isLocalMessage) + ) + } + ) + } +} + +@Composable +private fun MessageListScreenContent( + viewState: ViewState, + listState: LazyListState, + showOrderFeatureChangedLabel: Boolean, + onClickOrder: (String, Boolean) -> Unit, + onClickInfoLabel: () -> Unit, + onClickRetry: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.messages_title), + listState = listState + ) { + Orders( + listState = listState, + showOrderFeatureChangedLabel = showOrderFeatureChangedLabel, + ordersData = viewState.messagesList, + onClickInfoLabel = onClickInfoLabel, + onClickOrder = onClickOrder, + onClickRetry = onClickRetry + ) + } +} + +@LightDarkPreview +@Composable +fun MessageScreenContentPreview( + @PreviewParameter(MessageListParameterProvider::class) + ordersData: UiState> +) { + PreviewAppTheme { + MessageListScreenContent( + listState = rememberLazyListState(), + showOrderFeatureChangedLabel = false, + viewState = ViewState(false, ordersData), + onClickOrder = { _, _ -> }, + onClickInfoLabel = {}, + onClickRetry = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/MlKitModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/MlKitModule.kt index df4969e8..3a7d4765 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/MlKitModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/MlKitModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitGraph.kt index 00b4ebc4..7be0af23 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitRoutes.kt index 345cd4f0..8f2ab10d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/navigation/MlKitRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/presentation/MlkitController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/presentation/MlkitController.kt index 22d456cc..cf6f8d2e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/presentation/MlkitController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/presentation/MlkitController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreen.kt index 5733894a..2b44e554 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit.ui @@ -41,14 +41,15 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold import de.gematik.ti.erp.app.utils.compose.providePhoneIntent class MlKitInformationScreen( @@ -204,3 +205,20 @@ fun MlKitInformationScreenContentPreview() { ) } } + +@LightDarkPreview +@Composable +fun MlKitInformationScreenScaffoldPreview() { + val listState = rememberLazyListState() + PreviewAppTheme { + TestScaffold( + topBarTitle = stringResource(R.string.ml_information_title), + navigationMode = NavigationBarMode.Back + ) { + MlKitInformationScreenContent( + listState = listState, + contentPadding = PaddingValues(all = PaddingDefaults.Medium) + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreen.kt index d08affbd..ca47c925 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.mlkit.ui @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -40,7 +39,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.mlkit.navigation.MlKitRoutes import de.gematik.ti.erp.app.mlkit.presentation.rememberMlKitController @@ -48,21 +46,17 @@ import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme import de.gematik.ti.erp.app.utils.compose.PrimaryButton import de.gematik.ti.erp.app.utils.compose.SecondaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold -@Requirement( - "A_20194", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Display information regarding usage of MLKit to the user." -) class MlKitScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry @@ -164,22 +158,23 @@ private fun MlKitBottomBar(onAccept: () -> Unit, onClickReadMore: () -> Unit) { @LightDarkPreview @Composable -fun MlKitScreenContentPreview() { +fun MlKitScreenScaffoldPreview() { val listState = rememberLazyListState() PreviewAppTheme { - Scaffold( + TestScaffold( + topBarTitle = "", + navigationMode = NavigationBarMode.Close, bottomBar = { MlKitBottomBar( onAccept = {}, - onClickReadMore = {} - ) - }, - content = { - MlKitScreenContent( - listState = listState, - contentPadding = it + onClickReadMore = { } ) } - ) + ) { + MlKitScreenContent( + contentPadding = it, + listState = listState + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/AnimationUtils.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/AnimationUtils.kt index 45e44a6f..5165a1ba 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/AnimationUtils.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/AnimationUtils.kt @@ -1,28 +1,31 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally +import de.gematik.ti.erp.app.animated.AnimationTime private const val TARGET_OFFSET = -300 private const val INITIAL_OFFSET = 300 @@ -61,10 +64,7 @@ fun fadeInLong() = fadeIn(tween(durationMillis = TWEEN_DURATION)) fun fadeOutLong() = fadeOut(tween(durationMillis = TWEEN_DURATION)) -fun slideInAnimation( - animationTime: Long, - extraTime: Long = 1000 -) = slideInHorizontally( - initialOffsetX = { -it }, - animationSpec = tween(animationTime.toInt()) -) + fadeIn(animationSpec = tween(animationTime.toInt() + extraTime.toInt())) +fun oneSecondInfiniteTween() = infiniteRepeatable( + animation = tween(durationMillis = AnimationTime.ONE_SECOND, easing = LinearEasing), + repeatMode = RepeatMode.Reverse +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/BottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/BottomSheetScreen.kt index 36dc0a45..20c6d9ff 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/BottomSheetScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/BottomSheetScreen.kt @@ -1,53 +1,106 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("UsingMaterialAndMaterial3Libraries") + package de.gematik.ti.erp.app.navigation +import androidx.compose.material3.BottomSheetDefaults.DragHandle import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState import de.gematik.ti.erp.app.theme.AppTheme /** - * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is tall enough, - * should be skipped. If true, the sheet will always expand to the [Expanded] state and move to the - * [Hidden] state when hiding the sheet, either programmatically or by user interaction. - * - * @param allowStateChange Optional callback invoked to confirm or veto a pending state change. + * The {currentRoute?.destination?.navigatorName} gives the navigator name of the current route. + * This uses {composable} for screens and [BottomSheetNavigator] for bottom sheet screens. + */ +private const val BottomSheetNavigator = "BottomSheetNavigator" + +/** + * When a bottom-sheet screen is created and we add {Modifier.wrapContentSize()} to the content and + * [forceToMaxHeight] is set to true, the bottom sheet will take the full height which is available + * that is greater than the default height but less than the full screen height. */ abstract class BottomSheetScreen( - val skipPartiallyExpanded: Boolean = true, - val allowStateChange: Boolean = true + private val forceToMaxHeight: Boolean = false ) : Screen() { - @OptIn(ExperimentalMaterial3Api::class) @Composable fun BottomSheetContent() { - ModalBottomSheet( - onDismissRequest = { navController.popBackStack() }, - containerColor = AppTheme.colors.neutral000, - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = skipPartiallyExpanded, - confirmValueChange = { allowStateChange } - ) + Content( + navController = navController, + forceToMaxHeight = forceToMaxHeight ) { this@BottomSheetScreen.Content() } } } + +/** +Adding a bottom sheet inside the bottom-sheet navigation to allow forced full screen for smaller screens to allow +more than the allowed default height of the bottom sheet + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Content( + navController: NavController, + forceToMaxHeight: Boolean, + content: @Composable () -> Unit +) { + val modalBottomSheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = forceToMaxHeight) + + ModalBottomSheet( + sheetState = modalBottomSheetState, + onDismissRequest = navController::navigateUp, + containerColor = AppTheme.colors.neutral000, + contentColor = AppTheme.colors.neutral000, + dragHandle = { + DragHandle( + color = AppTheme.colors.neutral600 + ) + } + ) { + content() + } +} + +/** + * This function is used to hide the bottom sheet impromptu. + * It checks if the current route is a bottom sheet by checking its + * navigator name and then pops it off the back stack. + * This is used when we want to hide it when the user is asked to authenticate again. + */ +@Composable +@Suppress("ComposableNaming") +fun NavHostController.hideUnsafeBottomSheet() { + val currentRoute by currentBackStackEntryAsState() + val navigatorName = currentRoute?.destination?.navigatorName?.trim() + LaunchedEffect(navigatorName) { + if (navigatorName == BottomSheetNavigator) { + navigateUp() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/ErezeptNavigatorFactory.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/ErezeptNavigatorFactory.kt new file mode 100644 index 00000000..d930b9c2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/ErezeptNavigatorFactory.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.compose.rememberNavController +import com.google.accompanist.navigation.material.BottomSheetNavigator +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import de.gematik.ti.erp.app.navigation.model.ErezeptNavigatorHolder + +object ErezeptNavigatorFactory { + + @Composable + @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterialNavigationApi::class) + fun initNavigation(): ErezeptNavigatorHolder { + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + skipHalfExpanded = false + ) + + val bottomSheetNavigator = remember(sheetState) { BottomSheetNavigator(sheetState) } + + val navHostController = rememberNavController(bottomSheetNavigator) + + return ErezeptNavigatorHolder(navHostController, bottomSheetNavigator, sheetState) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavBackStackEntryExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavBackStackEntryExtensions.kt new file mode 100644 index 00000000..fe770791 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavBackStackEntryExtensions.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.NavBackStackEntry +import io.github.aakira.napier.Napier + +// This is a composable function that takes a NavBackStackEntry and a route and an action to be executed when +@Suppress("ComposableNaming") +@Composable +fun NavBackStackEntry.onReturnAction( + route: Routes, + onReturnAction: () -> Unit +) { + LaunchedEffect(this) { + Napier.d { "on returning to ${destination.route} looking for ${route.route}" } + if (destination.route == route.route) { + onReturnAction() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavControllerExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavControllerExtension.kt index c51c4ee0..0a1c5239 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavControllerExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavControllerExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation @@ -38,7 +38,6 @@ fun NavController.navigateAndClearStack( excludeUpTodRoute?.let { // pops up to the point that is given popUpTo(it) { - this.saveState inclusive = isInclusive } } ?: run { @@ -49,3 +48,27 @@ fun NavController.navigateAndClearStack( } } } + +/** + * [navigateAsSingleScreen] navigates to the given [route] + * @param route The route to be navigated to. + * restoreState If true, the state of the destination will be restored + * launchSingleTop If true, and the destination is on the top of the back stack, this destination + */ +fun NavController.navigateAsSingleScreen(route: String) { + navigate(route = route) { + launchSingleTop = true + restoreState = true + } +} + +fun NavController.replaceRoute(currentRoute: String, newRoute: String) { + // Pop up to the current route if it exists + if (this.currentDestination?.route == currentRoute) { + this.popBackStack() + } else { + this.popBackStack(currentRoute, inclusive = true) + } + // Navigate to the new route + this.navigate(newRoute) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt index 781302c2..4ef9d76b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation @@ -24,7 +24,6 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.ColumnScope import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry @@ -33,6 +32,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.google.accompanist.navigation.material.bottomSheet +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.tracker.Tracker import org.kodein.di.compose.rememberInstance @@ -71,28 +71,27 @@ fun NavGraphBuilder.renderComposable( )? = stackExitAnimation, screen: AnimatedContentScope.(NavBackStackEntry) -> Screen ) { + @Requirement( + "A_19094-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Tracking is built into the navigation system of the app where only screen names can be transmitted." + ) composable( route = route, arguments = arguments, deepLinks = deepLinks, - enterTransition = stackEnterAnimation ?: { slideInRight() }, - exitTransition = stackExitAnimation ?: { slideOutLeft() }, - popEnterTransition = popEnterAnimation ?: { slideInDown() }, - popExitTransition = popExitAnimation ?: { slideOutUp() }, + enterTransition = stackEnterAnimation, + exitTransition = stackExitAnimation, + popEnterTransition = popEnterAnimation ?: stackEnterAnimation, + popExitTransition = popExitAnimation ?: stackExitAnimation, content = { val screenToBeRendered = screen(this, it) val tracker by rememberInstance() - val routeToBeTracked = remember { route.routeEnum() } - val isTrackingPermitted = remember { mutableStateOf(false) } - val screenAnalyticsMetric = remember { mutableStateOf(null) } - - tracker.computeScreenTrackingProperty(routeToBeTracked) { trackingData -> - isTrackingPermitted.value = true - screenAnalyticsMetric.value = trackingData + tracker.computeScreenTrackingProperty(routeToBeTracked)?.let { screenName -> + tracker.trackScreen(screenName) } - tracker.track(screenAnalyticsMetric, isTrackingPermitted) screenToBeRendered.Content() } ) @@ -113,6 +112,11 @@ fun NavGraphBuilder.renderBottomSheet( deepLinks: List = emptyList(), screen: ColumnScope.(NavBackStackEntry) -> BottomSheetScreen ) { + @Requirement( + "A_19094-01#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Tracking is built into the navigation system of the app where only screen names can be transmitted." + ) bottomSheet( route = route, arguments = arguments, @@ -120,17 +124,10 @@ fun NavGraphBuilder.renderBottomSheet( content = { val screenToBeRendered = screen(this, it) val tracker by rememberInstance() - val routeToBeTracked = remember { route.routeEnum() } - val isTrackingPermitted = remember { mutableStateOf(false) } - val screenAnalyticsMetric = remember { mutableStateOf(null) } - - tracker.computeScreenTrackingProperty(routeToBeTracked) { trackingData -> - isTrackingPermitted.value = true - screenAnalyticsMetric.value = trackingData + tracker.computeScreenTrackingProperty(routeToBeTracked)?.let { screenName -> + tracker.trackScreen(screenName) } - - tracker.track(screenAnalyticsMetric, isTrackingPermitted) screenToBeRendered.BottomSheetContent() } ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationGraphBuilder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationGraphBuilder.kt new file mode 100644 index 00000000..a5dcdbaf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationGraphBuilder.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import com.google.accompanist.navigation.material.BottomSheetNavigator +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.google.accompanist.navigation.material.ModalBottomSheetLayout +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.extensions.navigationBarsWithImePadding + +@OptIn(ExperimentalMaterialNavigationApi::class) +@Composable +fun NavigationGraphBuilder( + bottomSheetNavigator: BottomSheetNavigator, + navHostController: NavHostController, + startDestination: String, + builder: NavGraphBuilder.() -> Unit +) { + ModalBottomSheetLayout( + modifier = Modifier.navigationBarsWithImePadding().systemBarsPadding(), + sheetShape = RoundedCornerShape(topStart = SizeDefaults.double, topEnd = SizeDefaults.double), + bottomSheetNavigator = bottomSheetNavigator + ) { + NavHost( + navHostController, + startDestination = startDestination + ) { + builder(this) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt index 537d17e0..a125b36b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt @@ -1,24 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation -enum class NavigationRouteNames { +enum class NavigationRouteNames( + val doNoTrack: Boolean = false +) { // App-security DeviceCheckLoadingScreen, InsecureDeviceScreen, @@ -28,9 +30,13 @@ enum class NavigationRouteNames { ProfileScreen, ProfileEditPictureScreen, ProfileImageCropperScreen, - ProfileTokenScreen, + ProfileImageEmojiScreen, + ProfileImageCameraScreen, ProfileAuditEventsScreen, ProfilePairedDevicesScreen, + ProfileEditPictureBottomSheetScreen, + ProfileEditNameBottomSheetScreen, + ProfileAddNameBottomSheetScreen, // Invoices/ pkv InvoiceListScreen, @@ -49,6 +55,17 @@ enum class NavigationRouteNames { PrescriptionDetailMedicationIngredientsScreen, PrescriptionDetailAccidentInfoScreen, PrescriptionDetailTechnicalInfoScreen, + PrescriptionDetailSelfPayerPrescriptionBottomSheetScreen, + PrescriptionDetailAdditionalFeeNotExemptBottomSheetScreen, + PrescriptionDetailAdditionalFeeExemptBottomSheetScreen, + PrescriptionDetailFailureBottomSheetScreen, + PrescriptionDetailScannedBottomSheetScreen, + PrescriptionDetailDirectAssignmentBottomSheetScreen, + PrescriptionDetailSubstitutionAllowedBottomSheetScreen, + PrescriptionDetailSubstitutionNotAllowedBottomSheetScreen, + PrescriptionDetailEmergencyFeeExemptBottomSheetScreen, + PrescriptionDetailEmergencyFeeNotExemptBottomSheetScreen, + PrescriptionDetailHowLongValidBottomSheetScreen, // Onboarding OnboardingWelcomeScreen, @@ -66,29 +83,35 @@ enum class NavigationRouteNames { // Settings SettingsScreen, - SettingsAccessibilityScreen, SettingsProductImprovementScreen, SettingsAllowAnalyticsScreen, - SettingsDeviceSecurityScreen, + SettingsAppSecurityScreen, SettingsSetAppPasswordScreen, SettingsDataProtectionScreen, SettingsTermsOfUseScreen, SettingsLegalNoticeScreen, SettingsOpenSourceLicencesScreen, SettingsAdditionalLicencesScreen, + SettingsLanguageScreen, // MlKit MlKit, MlKitInformationScreen, - // Main + // Prescriptions + PrescriptionsScreen, + PrescriptionsArchiveScreen, PrescriptionScanScreen, + WelcomeDrawerBottomSheetScreen, + GrantConsentBottomSheetScreen, - // sample screen - SampleOverviewScreen, - BottomSheetSampleScreen, - BottomSheetSampleSmallScreen, - BottomSheetSampleLargeScreen, + // Messages + MessageListScreen, + MessageDetailScreen, + MessageBottomSheetScreen, + + // showcase screen + BottomSheetShowcaseScreen, // tracker DemoTrackerScreen, @@ -108,7 +131,8 @@ enum class NavigationRouteNames { CardWallSaveCredentialsScreen, CardWallSaveCredentialsInfoScreen, CardWallReadCardScreen, - CardWallExternalAuthenticationScreen, + CardWallGidListScreen, + CardWallGidHelpScreen, // TroubleShooting TroubleShootingIntroScreen, @@ -121,10 +145,34 @@ enum class NavigationRouteNames { // Pharmacy PharmacyStartScreen, + PharmacyStartScreenModal, PharmacyFilterSheetScreen, PharmacySearchListScreen, PharmacySearchMapsScreen, - PharmacyOrderOverviewScreen, - PharmacyEditShippingContactScreen, - PharmacyPrescriptionSelectionScreen + PharmacyDetailsFromMessageScreen, + RedeemOrderOverviewScreen, + RedeemEditShippingContactScreen, + RedeemPrescriptionSelectionScreen, + PharmacyDetailsFromPharmacyScreen, + + // Redeem + RedeemMethodSelection, + RedeemPrescriptionSelection, + RedeemLocal, + RedeemOnline, + + // OrderHealthCard + OrderHealthCardSelectInsuranceCompanyScreen, + OrderHealthCardSelectOptionScreen, + OrderHealthCardSelectMethodScreen, + + // MedicationPlan + SuccessScreen, + ScheduleListScreen, + ScheduleScreen, + DosageInfoScreen, + ScheduleDateRangeScreen, + + // UserAuthentication + UserAuthenticationScreen, } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt index 99a2cb9a..83d94291 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt index 41bd37a4..fc2b6257 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt index ce2b85f3..eedb48c9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt @@ -1,35 +1,49 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation +import android.content.Context +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.SnackbarHostState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import kotlinx.coroutines.CoroutineScope /** * Screen is an abstract class that is used to as a screen object that is used * for navigation. - * @param navController is required to navigate to other screens - * @param navBackStackEntry is required to obtain args that we might get that is + * [navController] is required to navigate to other screens + * [navBackStackEntry] is required to obtain args that we might get that is * required for the screens * Method [Content] is the one used to the host the composable and show the content - * The internal method [computeScreenTrackingProperty] is used to track the screen by used the route + * The internal method [de.gematik.ti.erp.app.analytics.tracker.Tracker.computeScreenTrackingProperty] is + * used to track the screen by used the route * variable that is used for accessing the Screen */ @Suppress("UnnecessaryAbstractClass") @@ -39,9 +53,35 @@ abstract class Screen { abstract val navBackStackEntry: NavBackStackEntry + val context: Context + @Composable + get() = LocalContext.current + + val dialog: DialogScaffold + @Composable + get() = LocalDialog.current + + val snackbar: SnackbarHostState + @Composable + get() = LocalSnackbarScaffold.current + + val listState: LazyListState + @Composable + get() = rememberLazyListState() + + @OptIn(ExperimentalMaterial3Api::class) + val pullToRefreshState: PullToRefreshState + @Composable + get() = rememberPullToRefreshState() + + val scope: CoroutineScope + @Composable + get() = rememberCoroutineScope() + /** * The composable content that is shown in the screen should be placed in [Content] */ + @Suppress("LongMethod") @Composable abstract fun Content() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/model/ErezeptNavigatorHolder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/model/ErezeptNavigatorHolder.kt new file mode 100644 index 00000000..15a85a3e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/model/ErezeptNavigatorHolder.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.navigation.model + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.navigation.NavHostController +import com.google.accompanist.navigation.material.BottomSheetNavigator +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi + +@OptIn(ExperimentalMaterialApi::class) +data class ErezeptNavigatorHolder +@OptIn(ExperimentalMaterialNavigationApi::class) +constructor( + val navHostController: NavHostController, + val bottomSheetNavigator: BottomSheetNavigator, + val sheetState: ModalBottomSheetState +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/di/OnboardingModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/di/OnboardingModule.kt new file mode 100644 index 00000000..98a7d1e7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/di/OnboardingModule.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.di + +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController +import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingDataUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val onboardingModule = DI.Module("onboardingModule") { + bindProvider { GetOnboardingSucceededUseCase(instance()) } + bindProvider { SaveOnboardingDataUseCase(instance()) } + bindSingleton { + OnboardingGraphController( + instance(), + instance(), + instance(), + instance(), + instance(), + instance(), + instance(), + instance() + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt index 6658b416..ee42f040 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.model diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt deleted file mode 100644 index a0cdeed7..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.onboarding.model - -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.ui.validatePassword -import kotlinx.parcelize.Parcelize - -@Immutable -sealed class OnboardingSecureAppMethod { - @Immutable - @Parcelize - data class Password(val password: String, val repeatedPassword: String, val score: Int) : - OnboardingSecureAppMethod(), - Parcelable { - val checkedPassword: String? - get() = - when { - validatePassword(password, repeatedPassword, score) -> password - else -> null - } - } - - @Parcelize - object DeviceSecurity : OnboardingSecureAppMethod(), Parcelable - - @Parcelize - object None : OnboardingSecureAppMethod(), Parcelable - - companion object { - fun OnboardingSecureAppMethod.toAuthenticationMode() = - when (this) { - DeviceSecurity -> SettingsData.AuthenticationMode.DeviceSecurity - is Password -> SettingsData.AuthenticationMode.Password( - password = requireNotNull(checkedPassword) - ) - None -> SettingsData.AuthenticationMode.Unspecified - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingGraph.kt index 2061a205..4b533a78 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.navigation @@ -27,19 +27,27 @@ import de.gematik.ti.erp.app.navigation.fadeOutLong import de.gematik.ti.erp.app.navigation.renderComposable import de.gematik.ti.erp.app.navigation.slideInDown import de.gematik.ti.erp.app.navigation.slideOutUp +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.onboarding.ui.DataProtectionScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingAnalyticsPreviewScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingDataProtectionAndTermsOfUseOverviewScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingSelectAppLoginScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingWelcomeScreen import de.gematik.ti.erp.app.onboarding.ui.TermsOfUseScreen -import de.gematik.ti.erp.app.userauthentication.ui.BiometryScreen +import de.gematik.ti.erp.app.onboarding.ui.BiometryScreen +import org.kodein.di.DI +import org.kodein.di.instance fun NavGraphBuilder.onboardingGraph( - navController: NavController, - startDestination: String + dependencyInjector: DI, + startDestination: String = OnboardingRoutes.OnboardingWelcomeScreen.route, + navController: NavController ) { - navigation(startDestination = startDestination, route = OnboardingRoutes.subGraphName()) { + val controller by dependencyInjector.instance() + navigation( + startDestination = startDestination, + route = OnboardingRoutes.subGraphName() + ) { renderComposable( route = OnboardingRoutes.OnboardingWelcomeScreen.route, stackEnterAnimation = { fadeInLong() }, @@ -47,7 +55,8 @@ fun NavGraphBuilder.onboardingGraph( ) { OnboardingWelcomeScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -55,7 +64,8 @@ fun NavGraphBuilder.onboardingGraph( ) { OnboardingDataProtectionAndTermsOfUseOverviewScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -63,7 +73,8 @@ fun NavGraphBuilder.onboardingGraph( ) { OnboardingSelectAppLoginScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -71,7 +82,8 @@ fun NavGraphBuilder.onboardingGraph( ) { BiometryScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -79,7 +91,8 @@ fun NavGraphBuilder.onboardingGraph( ) { OnboardingAnalyticsPreviewScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -88,7 +101,8 @@ fun NavGraphBuilder.onboardingGraph( ) { OnboardingAllowAnalyticsScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -98,7 +112,8 @@ fun NavGraphBuilder.onboardingGraph( ) { TermsOfUseScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } renderComposable( @@ -108,7 +123,8 @@ fun NavGraphBuilder.onboardingGraph( ) { DataProtectionScreen( navController = navController, - navBackStackEntry = it + navBackStackEntry = it, + graphController = controller ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingNavigationExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingNavigationExtension.kt index 7f8f72f9..86498001 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingNavigationExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingNavigationExtension.kt @@ -1,30 +1,34 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.navigation import androidx.navigation.NavController -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes internal fun NavController.finishOnboardingAsSuccessAndOpenPrescriptions() { - navigateAndClearStack( - route = MainNavigationScreens.Prescriptions.path(), - excludeUpTodRoute = MainNavigationScreens.Onboarding.path() + navigate( + route = PrescriptionRoutes.PrescriptionsScreen.route, + navOptions = navOptions { + popUpTo(OnboardingRoutes.subGraphName()) { + inclusive = true + } + } ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingRoutes.kt index cd66e732..25da6d4d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/navigation/OnboardingRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingGraphController.kt new file mode 100644 index 00000000..0daf53cb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingGraphController.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.presentation + +import android.content.res.Resources +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase +import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase +import de.gematik.ti.erp.app.analytics.usecase.StartTrackerUseCase +import de.gematik.ti.erp.app.analytics.usecase.StopTrackerUseCase +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingDataUseCase +import de.gematik.ti.erp.app.utils.letNotNull +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +private const val PASSWORD_SCORE = 9 + +@Requirement( + "O.Data_1#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Viewmodel that is shared during the onboarding process to provide the user permissions.", + codeLines = 10 +) +class OnboardingGraphController( + private val allowScreenshotsUseCase: AllowScreenshotsUseCase, + private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, + private val getProfilesUseCase: GetProfilesUseCase, + private val saveOnboardingDataUseCase: SaveOnboardingDataUseCase, + private val startTrackerUseCase: StartTrackerUseCase, + private val stopTrackerUseCase: StopTrackerUseCase, + private val resources: Resources +) : Controller() { + private val skipOnboardingWithAuthenticationPassword = SettingsData.Authentication( + deviceSecurity = false, + failedAuthenticationAttempts = 0, + password = SettingsData.Authentication.Password("password") + ) + + private val authentication = MutableStateFlow(null) + + private val profileName = MutableStateFlow(resources.getString(R.string.onboarding_default_profile_name)) + + private val isAnalyticsAllowed by lazy { + isAnalyticsAllowedUseCase.invoke() + } + + val isAnalyticsAllowedState + @Composable + get() = isAnalyticsAllowed.collectAsStateWithLifecycle( + initialValue = false, + minActiveState = Lifecycle.State.RESUMED + ) + + fun onChooseAuthentication( + authentication: SettingsData.Authentication + ) { + this.authentication.update { authentication } + } + + fun createProfile() = controllerScope.launch { + letNotNull( + authentication.value, + profileName.value + ) { auth, name -> + saveOnboardingDataUseCase( + authentication = auth, + profileName = name + ) + } + } + + @Requirement( + "O.Data_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Permissions are asked and obtained during the onboarding process.", + codeLines = 5 + ) + fun allowScreenshots(allow: Boolean) { + controllerScope.launch { + allowScreenshotsUseCase(allow) + } + } + + @Requirement( + "O.Data_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Permissions are asked and obtained during the onboarding process.", + codeLines = 5 + ) + fun changeAnalyticsState(state: Boolean) { + controllerScope.launch { + changeAnalyticsStateUseCase.invoke(state) + when { + state -> startTrackerUseCase() + else -> stopTrackerUseCase() + } + } + } + + fun createProfileOnSkipOnboarding() { + changeAnalyticsState(false) + this.authentication.value = skipOnboardingWithAuthenticationPassword + this.profileName.value = resources.getString(R.string.onboarding_default_profile_name) + controllerScope.launch { + val profileList = getProfilesUseCase.invoke().first() + // avoid creating new profiles when it is already existing + if (profileList.isEmpty()) { + createProfile() + } + } + } +} + +@Composable +fun rememberOnboardingController(): OnboardingGraphController { + val getProfilesUseCase by rememberInstance() + val saveOnboardingSucceededUseCase by rememberInstance() + val allowScreenshotsUseCase by rememberInstance() + val isAnalyticsAllowedUseCase by rememberInstance() + val changeAnalyticsStateUseCase by rememberInstance() + val startTrackerUseCase by rememberInstance() + val stopTrackerUseCase by rememberInstance() + val resources by rememberInstance() + + return remember { + OnboardingGraphController( + getProfilesUseCase = getProfilesUseCase, + saveOnboardingDataUseCase = saveOnboardingSucceededUseCase, + allowScreenshotsUseCase = allowScreenshotsUseCase, + isAnalyticsAllowedUseCase = isAnalyticsAllowedUseCase, + changeAnalyticsStateUseCase = changeAnalyticsStateUseCase, + startTrackerUseCase = startTrackerUseCase, + stopTrackerUseCase = stopTrackerUseCase, + resources = resources + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingSharedController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingSharedController.kt deleted file mode 100644 index 9d7bb482..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/presentation/OnboardingSharedController.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.onboarding.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase -import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.Companion.toAuthenticationMode -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.None -import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingDataUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -private const val PASSWORD_SCORE = 9 - -class OnboardingSharedController( - private val allowScreenshotsUseCase: AllowScreenshotsUseCase, - private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, - private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, - private val getProfilesUseCase: GetProfilesUseCase, - private val saveOnboardingDataUseCase: SaveOnboardingDataUseCase, - private val defaultProfileName: String, - private val scope: CoroutineScope -) { - private val mutableSecureAppMethod = MutableStateFlow(None) - - private val skipPasswordAppMethod = OnboardingSecureAppMethod.Password("a", "a", PASSWORD_SCORE) - - private val isAnalyticsAllowed by lazy { - isAnalyticsAllowedUseCase.invoke() - } - - val isAnalyticsAllowedState - @Composable - get() = isAnalyticsAllowed.collectAsStateWithLifecycle( - initialValue = false, - minActiveState = Lifecycle.State.RESUMED - ) - - val secureAppMethod - @Composable - get() = mutableSecureAppMethod.collectAsStateWithLifecycle() - - fun updateAuthenticationMode(mode: OnboardingSecureAppMethod) { - mutableSecureAppMethod.value = mode - } - - fun onSaveOnboardingData( - authenticationMode: SettingsData.AuthenticationMode, - profileName: String - ) = scope.launch { - saveOnboardingDataUseCase( - authenticationMode = authenticationMode, - profileName = profileName - ) - } - - fun allowScreenshots(allow: Boolean) { - scope.launch { - allowScreenshotsUseCase(allow) - } - } - - fun changeAnalyticsState(state: Boolean) { - scope.launch { - changeAnalyticsStateUseCase.invoke(state) - } - } - - fun createProfileOnSkipOnboarding() { - changeAnalyticsState(false) - updateAuthenticationMode(skipPasswordAppMethod) - scope.launch { - val profileList = getProfilesUseCase.invoke().first() - if (profileList.isEmpty()) { - // avoid creating new profiles when it is already existing - onSaveOnboardingData( - authenticationMode = skipPasswordAppMethod.toAuthenticationMode(), - profileName = defaultProfileName - ) - } - } - } -} - -@Composable -fun rememberOnboardingController(): OnboardingSharedController { - val getProfilesUseCase by rememberInstance() - val saveOnboardingSucceededUseCase by rememberInstance() - val allowScreenshotsUseCase by rememberInstance() - val isAnalyticsAllowedUseCase by rememberInstance() - val changeAnalyticsStateUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - val defaultProfileName = stringResource(R.string.onboarding_default_profile_name) - - return remember { - OnboardingSharedController( - getProfilesUseCase = getProfilesUseCase, - saveOnboardingDataUseCase = saveOnboardingSucceededUseCase, - allowScreenshotsUseCase = allowScreenshotsUseCase, - isAnalyticsAllowedUseCase = isAnalyticsAllowedUseCase, - changeAnalyticsStateUseCase = changeAnalyticsStateUseCase, - defaultProfileName = defaultProfileName, - scope = scope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt index 3a7795fc..bf471dbb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreen.kt new file mode 100644 index 00000000..99ad2dc1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreen.kt @@ -0,0 +1,228 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import android.app.KeyguardManager +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.authentication.presentation.deviceBiometricStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceDeviceSecurityStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceHasAuthenticationMethodEnabled +import de.gematik.ti.erp.app.authentication.ui.components.EnrollBiometricDialog +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog + +class BiometryScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController +) : Screen() { + + @Composable + override fun Content() { + val lazyListState = rememberLazyListState() + + val activity = LocalActivity.current + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } + val promptInfo = biometricPromptBuilder.buildPromptInfoWithAllAuthenticatorsAvailable( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info) + ) + val prompt = remember(biometricPromptBuilder) { + biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + graphController.onChooseAuthentication( + authentication = SettingsData.Authentication( + deviceSecurity = true, + failedAuthenticationAttempts = 0, + password = null + ) + ) + navController.navigate(OnboardingRoutes.OnboardingAnalyticsPreviewScreen.path()) + } + ) + } + + val context = LocalContext.current + val dialog = LocalDialog.current + val showEnrollBiometricEvent = ComposableEvent() + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val deviceHasDeviceSecurityEnabled by remember { + mutableStateOf( + keyguardManager.isDeviceSecure or deviceHasAuthenticationMethodEnabled(context.deviceDeviceSecurityStatus()) + ) + } + val deviceHasBiometryEnabled by remember { + mutableStateOf( + deviceHasAuthenticationMethodEnabled(context.deviceBiometricStatus()) + ) + } + + BiometricScreenScaffold( + lazyListState = lazyListState, + onBottomBarButtonClick = { + if (context.deviceDeviceSecurityStatus() == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) { + showEnrollBiometricEvent.trigger(Unit) + } else { + prompt.authenticate(promptInfo) + } + }, + deviceHasDeviceSecurityEnabled = deviceHasDeviceSecurityEnabled, + deviceHasBiometryEnabled = deviceHasBiometryEnabled, + onBack = { navController.popBackStack() } + ) + + EnrollBiometricDialog( + context = context, + dialog = dialog, + title = stringResource(R.string.enroll_biometric_dialog_header), + body = stringResource(R.string.enroll_biometric_dialog_body), + event = showEnrollBiometricEvent + ) + } +} + +@Composable +fun BiometricScreenScaffold( + lazyListState: LazyListState, + onBottomBarButtonClick: () -> Unit, + deviceHasDeviceSecurityEnabled: Boolean, + deviceHasBiometryEnabled: Boolean, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.navigationBarsPadding(), + navigationMode = NavigationBarMode.Close, + bottomBar = { + OnboardingBottomBar( + buttonText = stringResource(R.string.settings_device_security_allow), + onButtonClick = onBottomBarButtonClick, + buttonEnabled = true, + info = null, + buttonModifier = Modifier + ) + }, + topBarTitle = stringResource(R.string.settings_biometric_dialog_headline), + listState = lazyListState, + onBack = onBack + ) { + BiometricScreenContent( + lazyListState = lazyListState, + deviceHasDeviceSecurityEnabled = deviceHasDeviceSecurityEnabled, + deviceHasBiometryEnabled = deviceHasBiometryEnabled + ) + } +} + +@Composable +fun BiometricScreenContent( + lazyListState: LazyListState, + deviceHasDeviceSecurityEnabled: Boolean, + deviceHasBiometryEnabled: Boolean +) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .wrapContentSize() + .padding(horizontal = PaddingDefaults.Medium) + ) { + item { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .semantics(mergeDescendants = true) {} + ) { + Text( + stringResource(R.string.settings_biometric_dialog_title), + style = AppTheme.typography.h6, + modifier = Modifier.padding( + top = PaddingDefaults.Medium, + bottom = PaddingDefaults.Large + ) + ) + if (deviceHasDeviceSecurityEnabled && !deviceHasBiometryEnabled) { + Text( + text = stringResource(R.string.settings_device_security_dialog_text), + style = AppTheme.typography.body1, + modifier = Modifier.padding( + bottom = PaddingDefaults.Small + ) + ) + } else { + Text( + text = stringResource(R.string.settings_biometric_dialog_text), + style = AppTheme.typography.body1, + modifier = Modifier.padding( + bottom = PaddingDefaults.Small + ) + ) + } + } + } + } +} + +@LightDarkPreview() +@Composable +fun BiometryScreenPreview() { + PreviewAppTheme { + BiometricScreenScaffold( + lazyListState = rememberLazyListState(), + onBottomBarButtonClick = {}, + deviceHasBiometryEnabled = true, + deviceHasDeviceSecurityEnabled = true, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DataProtectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DataProtectionScreen.kt index 2db374e3..290905c9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DataProtectionScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DataProtectionScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -29,28 +29,29 @@ import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension import de.gematik.ti.erp.app.utils.extensions.getUriDataTerms import de.gematik.ti.erp.app.webview.WebViewScreen @Requirement( - "O.Purp_1#2", "O.Arch_8#6", - "O.Plat_11#6", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display data privacy as part of the onboarding. " + - "Webview containing local html without javascript." + rationale = "Webview containing local html without javascript." +) +@Requirement( + "O.Purp_1#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display data privacy as part of the onboarding. " ) class DataProtectionScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { @Composable override fun Content() { - val controller = rememberOnboardingController() - WebViewScreen( modifier = Modifier.testTag(TestTag.Onboarding.DataProtectionScreen), title = stringResource(R.string.onb_data_consent), @@ -59,7 +60,7 @@ class DataProtectionScreen( ) if (BuildConfigExtension.isNonReleaseMode) { SkipOnBoardingButton { - controller.createProfileOnSkipOnboarding() + graphController.createProfileOnSkipOnboarding() navController.finishOnboardingAsSuccessAndOpenPrescriptions() } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DepreacatedOnboardingNavigationScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DepreacatedOnboardingNavigationScreens.kt deleted file mode 100644 index 7fc1b13f..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/DepreacatedOnboardingNavigationScreens.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("MaximumLineLength") - -package de.gematik.ti.erp.app.onboarding.ui - -import de.gematik.ti.erp.app.navigation.Routes - -@Deprecated( - message = "Onboarding screens have been updated. For routes check OnboardingRoutes", - replaceWith = ReplaceWith("de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes") -) -object DeprecatedOnboardingNavigationScreens { - @Deprecated( - message = "Onboarding screens have been updated. For routes check OnboardingRoutes", - replaceWith = ReplaceWith("de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes") - ) - object Onboarding : Routes("onboarding") -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticsPreviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticsPreviewScreen.kt index 97707521..898fb9e9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticsPreviewScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticsPreviewScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Icon @@ -40,8 +39,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement @@ -50,15 +53,15 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.SwitchWithText -import de.gematik.ti.erp.app.utils.compose.visualTestTag +import de.gematik.ti.erp.app.utils.compose.SwitchLeftWithText +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Requirement( "O.Purp_3#4", @@ -67,49 +70,63 @@ import de.gematik.ti.erp.app.utils.compose.visualTestTag ) class OnboardingAnalyticsPreviewScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { - @Composable override fun Content() { - val controller = rememberOnboardingController() - val isAnalyticsAllowedState by controller.isAnalyticsAllowedState + val isAnalyticsAllowedState by graphController.isAnalyticsAllowedState var isAnalyticsAllowed by remember(isAnalyticsAllowedState) { mutableStateOf(isAnalyticsAllowedState) } - OnboardingScaffold( - state = rememberLazyListState(), - bottomBar = { - OnboardingBottomBar( - info = stringResource(R.string.onboarding_analytics_bottom_you_can_change), - buttonText = stringResource(R.string.onboarding_bottom_button_next), - buttonEnabled = true, - buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), - onButtonClick = { - // we finish onboarding irrespective of the analytics status - navController.finishOnboardingAsSuccessAndOpenPrescriptions() - } - ) - }, - modifier = Modifier - .visualTestTag(TestTag.Onboarding.AnalyticsScreen) - .fillMaxSize() - ) { - onboardingAnalyticsPreviewContent( - isAnalyticsAllowed = isAnalyticsAllowed, - onAnalyticsCheckChanged = { - isAnalyticsAllowed = it - controller.changeAnalyticsState(it) - if (it) { - navController.navigate(OnboardingRoutes.AllowAnalyticsScreen.path()) - } + OnboardingScreenScaffold( + isAnalyticsAllowed = isAnalyticsAllowed, + onAnalyticsCheckChanged = { + isAnalyticsAllowed = it + graphController.changeAnalyticsState(it) + if (it) { + navController.navigate(OnboardingRoutes.AllowAnalyticsScreen.path()) } + }, + onButtonClick = { + graphController.createProfile() + navController.finishOnboardingAsSuccessAndOpenPrescriptions() + } + ) + } +} + +@Composable +fun OnboardingScreenScaffold( + isAnalyticsAllowed: Boolean, + onAnalyticsCheckChanged: (Boolean) -> Unit, + onButtonClick: () -> Unit +) { + OnboardingScreenScaffold( + state = rememberLazyListState(), + bottomBar = { + OnboardingBottomBar( + info = stringResource(R.string.onboarding_analytics_bottom_you_can_change), + buttonText = stringResource(R.string.onboarding_bottom_button_next), + buttonEnabled = true, + buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), + onButtonClick = onButtonClick ) - } + }, + modifier = Modifier + .fillMaxSize() + ) { + onboardingAnalyticsPreviewContent( + isAnalyticsAllowed = isAnalyticsAllowed, + onAnalyticsCheckChanged = onAnalyticsCheckChanged + ) } } @Composable -private fun AnalyticsInfo(icon: ImageVector, text: String) { +private fun AnalyticsInfo( + icon: ImageVector, + text: String +) { Row(Modifier.fillMaxWidth()) { Icon(icon, null, tint = AppTheme.colors.primary600) SpacerMedium() @@ -131,7 +148,8 @@ private fun LazyListScope.onboardingAnalyticsPreviewContent( style = AppTheme.typography.h4, fontWeight = FontWeight.W700, textAlign = TextAlign.Start, - modifier = Modifier + modifier = + Modifier .padding( top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Large @@ -176,8 +194,11 @@ private fun LazyListScope.onboardingAnalyticsPreviewContent( SpacerMedium() } item { - SwitchWithText( - modifier = Modifier.testTag(TestTag.Onboarding.AnalyticsSwitch), + SwitchLeftWithText( + modifier = + Modifier.testTag(TestTag.Onboarding.AnalyticsSwitch).semantics { + toggleableState = ToggleableState.Off + }, text = stringResource(R.string.on_boarding_page_5_label), checked = isAnalyticsAllowed, onCheckedChange = onAnalyticsCheckChanged @@ -188,13 +209,14 @@ private fun LazyListScope.onboardingAnalyticsPreviewContent( @LightDarkPreview @Composable -fun OnboardingAnalyticsPreviewContentPreview() { +fun OnboardingAnalyticsPreviewContentPreview( + @PreviewParameter(BooleanPreviewParameterProvider::class) isAnalyticsAllowed: Boolean +) { PreviewAppTheme { - LazyColumn { - onboardingAnalyticsPreviewContent( - isAnalyticsAllowed = false, - onAnalyticsCheckChanged = {} - ) - } + OnboardingScreenScaffold( + isAnalyticsAllowed = isAnalyticsAllowed, + onAnalyticsCheckChanged = {}, + onButtonClick = {} + ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingBottomBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingBottomBar.kt index 025e53ee..c85e3895 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingBottomBar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingBottomBar.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -30,10 +30,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerShortMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall @Composable fun OnboardingBottomBar( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreen.kt index 37b9bf64..aa0d2616 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -22,8 +22,8 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -36,8 +36,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement @@ -46,78 +50,92 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme import de.gematik.ti.erp.app.utils.compose.SecondaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.SwitchWithText -import de.gematik.ti.erp.app.utils.compose.visualTestTag +import de.gematik.ti.erp.app.utils.compose.SwitchLeftWithText +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension -@Requirement( - "A_19184", - "A_20194", - "A_19980", - "A_19981", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Displays terms of service and privacy statement to the user." -) class OnboardingDataProtectionAndTermsOfUseOverviewScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { + @Requirement( + "O.Arch_9#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display data protection as part of the onboarding" + ) @Composable override fun Content() { var accepted by rememberSaveable { mutableStateOf(false) } + val lazyListState = rememberLazyListState() - val controller = rememberOnboardingController() - - OnboardingScaffold( - state = rememberLazyListState(), - bottomBar = { - OnboardingBottomBar( - modifier = Modifier.fillMaxWidth(), - info = null, - buttonText = stringResource(R.string.onboarding_bottom_button_accept), - buttonEnabled = accepted, - buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), - onButtonClick = { - navController.navigate(OnboardingRoutes.OnboardingSelectAppLoginScreen.path()) - } - ) + OnboardingDataProtectionAndTermsOfUseOverviewScreenContent( + lazyListState = lazyListState, + isAccepted = accepted, + onAcceptanceStateChanged = { accepted = it }, + onClickOnboardingBottomBar = { + navController.navigate(OnboardingRoutes.OnboardingSelectAppLoginScreen.path()) }, - modifier = Modifier - .visualTestTag(TestTag.Onboarding.DataTermsScreen) - .fillMaxSize() - ) { - dataProtectionAndTermsOfUseOverviewScreenContent( - isAccepted = accepted, - onCheckedChange = { - accepted = it - }, - onClickOpenDataProtectionButton = { - navController.navigate(OnboardingRoutes.DataProtectionScreen.path()) - }, - onClickOpenTermsOfUseButton = { - navController.navigate(OnboardingRoutes.TermsOfUseScreen.path()) - } - ) - } + onClickOpenDataProtectionButton = { + navController.navigate(OnboardingRoutes.DataProtectionScreen.path()) + }, + onClickOpenTermsOfUseButton = { + navController.navigate(OnboardingRoutes.TermsOfUseScreen.path()) + } + ) if (BuildConfigExtension.isNonReleaseMode) { SkipOnBoardingButton { - controller.createProfileOnSkipOnboarding() + graphController.createProfileOnSkipOnboarding() navController.finishOnboardingAsSuccessAndOpenPrescriptions() } } } } +@Composable +private fun OnboardingDataProtectionAndTermsOfUseOverviewScreenContent( + lazyListState: LazyListState, + isAccepted: Boolean, + onAcceptanceStateChanged: (Boolean) -> Unit, + onClickOnboardingBottomBar: () -> Unit, + onClickOpenDataProtectionButton: () -> Unit, + onClickOpenTermsOfUseButton: () -> Unit +) { + OnboardingScreenScaffold( + state = lazyListState, + bottomBar = { + OnboardingBottomBar( + modifier = Modifier.fillMaxWidth(), + info = null, + buttonText = stringResource(R.string.onboarding_bottom_button_accept), + buttonEnabled = isAccepted, + buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), + onButtonClick = onClickOnboardingBottomBar + ) + }, + modifier = Modifier + .testTag(TestTag.Onboarding.DataTermsScreen) + .fillMaxSize() + ) { + dataProtectionAndTermsOfUseOverviewScreenContent( + isAccepted = isAccepted, + onCheckedChange = onAcceptanceStateChanged, + onClickOpenDataProtectionButton = onClickOpenDataProtectionButton, + onClickOpenTermsOfUseButton = onClickOpenTermsOfUseButton + ) + } +} + private fun LazyListScope.dataProtectionAndTermsOfUseOverviewScreenContent( isAccepted: Boolean, onCheckedChange: (Boolean) -> Unit, @@ -147,15 +165,8 @@ private fun LazyListScope.dataProtectionAndTermsOfUseOverviewScreenContent( item { @Requirement( "O.Purp_3#1", - "O.Arch_9", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display data protection as part of the onboarding" - ) - @Requirement( - "A_19980#1", - "A_19981#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Display data protection as part of the onboarding" + rationale = "Button to navigate to data protection screen" ) ( SecondaryButton( @@ -173,7 +184,7 @@ private fun LazyListScope.dataProtectionAndTermsOfUseOverviewScreenContent( @Requirement( "O.Purp_3#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display terms of use as part of the onboarding" + rationale = "Button to navigate to terms of use screen" ) ( SecondaryButton( @@ -193,15 +204,11 @@ private fun LazyListScope.dataProtectionAndTermsOfUseOverviewScreenContent( sourceSpecification = "BSI-eRp-ePA", rationale = "User acceptance for terms of use and dar´ta protection as part of the onboarding" ) - @Requirement( - "A_19980#2", - "A_19981#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "The user is informed and required to accept this information via the data protection " + - "statement. Related data and services are listed in sections 5." - ) - SwitchWithText( - modifier = Modifier.testTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch), + SwitchLeftWithText( + modifier = + Modifier.testTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch).semantics { + toggleableState = ToggleableState.Off + }, text = stringResource(R.string.onboarding_data_terms_info), checked = isAccepted, onCheckedChange = onCheckedChange @@ -212,30 +219,17 @@ private fun LazyListScope.dataProtectionAndTermsOfUseOverviewScreenContent( @LightDarkPreview @Composable -fun DataProtectionAndTermsOfUseOverviewScreenContentFalsePreview() { - PreviewAppTheme { - LazyColumn { - dataProtectionAndTermsOfUseOverviewScreenContent( - isAccepted = false, - onCheckedChange = {}, - onClickOpenTermsOfUseButton = {}, - onClickOpenDataProtectionButton = {} - ) - } - } -} - -@LightDarkPreview -@Composable -fun DataProtectionAndTermsOfUseOverviewScreenContentTruePreview() { +fun OnboardingDataProtectionAndTermsOfUseOverviewScreenContentPreview( + @PreviewParameter(BooleanPreviewParameterProvider::class) isAccepted: Boolean +) { PreviewAppTheme { - LazyColumn { - dataProtectionAndTermsOfUseOverviewScreenContent( - isAccepted = true, - onCheckedChange = {}, - onClickOpenTermsOfUseButton = {}, - onClickOpenDataProtectionButton = {} - ) - } + OnboardingDataProtectionAndTermsOfUseOverviewScreenContent( + lazyListState = rememberLazyListState(), + isAccepted = isAccepted, + onAcceptanceStateChanged = {}, + onClickOnboardingBottomBar = {}, + onClickOpenDataProtectionButton = {}, + onClickOpenTermsOfUseButton = {} + ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingLazyColumn.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingLazyColumn.kt index cf6bd5ac..e1ba1f9e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingLazyColumn.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingLazyColumn.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt deleted file mode 100644 index 34114bae..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.onboarding.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import de.gematik.ti.erp.app.theme.PaddingDefaults - -@Composable -fun OnboardingScaffold( - modifier: Modifier = Modifier, - state: LazyListState, - bottomBar: @Composable () -> Unit, - content: LazyListScope.() -> Unit -) { - Scaffold( - modifier.systemBarsPadding(), - bottomBar = bottomBar - ) { innerPadding -> - val contentPadding by derivedStateOf { - PaddingValues( - bottom = innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - } - OnboardingLazyColumn( - modifier = Modifier.fillMaxSize(), - state = state, - content = content, - contentPadding = contentPadding - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScreenScaffold.kt new file mode 100644 index 00000000..7e9faa63 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScreenScaffold.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun OnboardingScreenScaffold( + modifier: Modifier = Modifier, + state: LazyListState, + bottomBar: @Composable () -> Unit, + content: LazyListScope.() -> Unit +) { + Scaffold( + modifier.systemBarsPadding(), + bottomBar = bottomBar + ) { innerPadding -> + val contentPadding by remember { + derivedStateOf { + PaddingValues( + bottom = innerPadding.calculateBottomPadding(), + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + } + } + OnboardingLazyColumn( + modifier = Modifier.fillMaxSize(), + state = state, + content = content, + contentPadding = contentPadding + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSelectAppLoginScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSelectAppLoginScreen.kt index 1035b4b4..e9de409c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSelectAppLoginScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSelectAppLoginScreen.kt @@ -1,23 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui +import android.app.KeyguardManager +import android.content.Context import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform import androidx.compose.foundation.Image @@ -43,37 +45,45 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.presentation.deviceBiometricStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceDeviceSecurityStatus +import de.gematik.ti.erp.app.authentication.presentation.deviceHasAuthenticationMethodEnabled +import de.gematik.ti.erp.app.authentication.presentation.deviceSupportsAuthenticationMethod import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.mainscreen.ui.TextTabRow import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.onboarding.model.OnboardingAuthTab -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.Companion.toAuthenticationMode -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.None -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.Password import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameter +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameterProvider import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny import de.gematik.ti.erp.app.utils.compose.ConfirmationPasswordTextField import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.PasswordStrength import de.gematik.ti.erp.app.utils.compose.PasswordTextField -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.validatePasswordScore +import de.gematik.ti.erp.app.utils.compose.presentation.PasswordFieldsData +import de.gematik.ti.erp.app.utils.compose.presentation.rememberPasswordFieldsController +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension private const val POS_OF_ANIMATED_CONTENT_ITEM = 3 @@ -83,92 +93,152 @@ private const val FAILURE_SCORE = 0 class OnboardingSelectAppLoginScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { @Composable override fun Content() { - val controller = rememberOnboardingController() - val secureAppMethod by controller.secureAppMethod - val profileName = stringResource(R.string.onboarding_default_profile_name) + val passwordFieldsController = rememberPasswordFieldsController() + val passwordFieldsState by passwordFieldsController.passwordFieldsState.collectAsStateWithLifecycle() val lazyListState = rememberLazyListState() var selectedTab by remember { mutableStateOf(OnboardingAuthTab.Biometric) } + val context = LocalContext.current - OnboardingScaffold( - modifier = Modifier - .testTag(TestTag.Onboarding.CredentialsScreen) - .fillMaxSize(), - state = lazyListState, - bottomBar = { - OnboardingBottomBar( - info = when (selectedTab) { - OnboardingAuthTab.Password -> null - OnboardingAuthTab.Biometric -> stringResource(R.string.onboarding_auth_biometric_info) - }, - buttonText = when (selectedTab) { - OnboardingAuthTab.Password -> stringResource(R.string.onboarding_bottom_button_save) - OnboardingAuthTab.Biometric -> stringResource(R.string.onboarding_bottom_button_choose) - }, - buttonEnabled = when (selectedTab) { - OnboardingAuthTab.Password -> (secureAppMethod as? Password)?.checkedPassword != null - OnboardingAuthTab.Biometric -> true - }, - buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), - onButtonClick = { - when (selectedTab) { - OnboardingAuthTab.Password -> { - controller.onSaveOnboardingData( - authenticationMode = secureAppMethod.toAuthenticationMode(), - profileName = profileName - ) - navController.navigate(OnboardingRoutes.OnboardingAnalyticsPreviewScreen.path()) - } - OnboardingAuthTab.Biometric -> { - navController.navigate(OnboardingRoutes.BiometricScreen.path()) - } - } - } + val deviceSupportsDeviceSecurity by remember { + mutableStateOf( + deviceSupportsAuthenticationMethod(context.deviceDeviceSecurityStatus()) + ) + } + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val deviceHasDeviceSecurityEnabled by remember { + mutableStateOf( + keyguardManager.isDeviceSecure or deviceHasAuthenticationMethodEnabled(context.deviceDeviceSecurityStatus()) + ) + } + val deviceHasBiometryEnabled by remember { + mutableStateOf( + deviceHasAuthenticationMethodEnabled(context.deviceBiometricStatus()) + ) + } + + OnboardingScreenContent( + selectedTab = selectedTab, + passwordFieldsState = passwordFieldsState, + lazyListState = lazyListState, + deviceSupportsDeviceSecurity = deviceSupportsDeviceSecurity, + deviceHasDeviceSecurityEnabled = deviceHasDeviceSecurityEnabled, + deviceHasBiometryEnabled = deviceHasBiometryEnabled, + onPasswordChange = passwordFieldsController::onPasswordChange, + onRepeatedPasswordChange = passwordFieldsController::onRepeatedPasswordChange, + onTabChange = { + when (it) { + 0 -> selectedTab = OnboardingAuthTab.Biometric + 1 -> selectedTab = OnboardingAuthTab.Password + } + }, + onChoosePassword = { + graphController.onChooseAuthentication( + authentication = SettingsData.Authentication( + deviceSecurity = false, + failedAuthenticationAttempts = 0, + password = SettingsData.Authentication.Password(passwordFieldsState.password) + ) ) + navController.navigate(OnboardingRoutes.OnboardingAnalyticsPreviewScreen.path()) + }, + onChooseDeviceSecurity = { + navController.navigate(OnboardingRoutes.BiometricScreen.path()) + }, + onSkip = { + graphController.createProfileOnSkipOnboarding() + navController.finishOnboardingAsSuccessAndOpenPrescriptions() } - ) { - onboardingSelectAppLoginContent( - selectedTab = selectedTab, - secureMethod = secureAppMethod, - lazyListState = lazyListState, - onSecureMethodChange = controller::updateAuthenticationMode, - onTabChange = { - when (it) { - 0 -> selectedTab = OnboardingAuthTab.Biometric - 1 -> selectedTab = OnboardingAuthTab.Password + ) + } +} + +@Composable +fun OnboardingScreenContent( + selectedTab: OnboardingAuthTab, + passwordFieldsState: PasswordFieldsData, + lazyListState: LazyListState, + deviceSupportsDeviceSecurity: Boolean, + deviceHasDeviceSecurityEnabled: Boolean, + deviceHasBiometryEnabled: Boolean, + onPasswordChange: (String) -> Unit, + onRepeatedPasswordChange: (String) -> Unit, + onTabChange: (Int) -> Unit, + onChoosePassword: () -> Unit, + onChooseDeviceSecurity: () -> Unit, + onSkip: () -> Unit +) { + OnboardingScreenScaffold( + modifier = Modifier + .testTag(TestTag.Onboarding.CredentialsScreen) + .fillMaxSize(), + state = lazyListState, + bottomBar = { + OnboardingBottomBar( + info = when (selectedTab) { + OnboardingAuthTab.Password -> null + OnboardingAuthTab.Biometric -> if (deviceSupportsDeviceSecurity) { + stringResource(R.string.onboarding_auth_biometric_info) + } else { + stringResource(R.string.auth_no_biometry_info) } - controller.updateAuthenticationMode(None) }, - onNext = { - controller.onSaveOnboardingData( - authenticationMode = secureAppMethod.toAuthenticationMode(), - profileName = profileName - ) - navController.navigate(OnboardingRoutes.OnboardingAnalyticsPreviewScreen.path()) + buttonText = when (selectedTab) { + OnboardingAuthTab.Password -> stringResource(R.string.onboarding_bottom_button_save) + OnboardingAuthTab.Biometric -> stringResource(R.string.onboarding_bottom_button_choose) + }, + buttonEnabled = when (selectedTab) { + OnboardingAuthTab.Password -> passwordFieldsState.passwordIsValidAndConsistent + OnboardingAuthTab.Biometric -> deviceSupportsDeviceSecurity + }, + buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), + onButtonClick = { + when (selectedTab) { + OnboardingAuthTab.Password -> onChoosePassword() + OnboardingAuthTab.Biometric -> onChooseDeviceSecurity() + } } ) } + ) { + onboardingSelectAppLoginContent( + selectedTab = selectedTab, + passwordFieldsState = passwordFieldsState, + deviceHasDeviceSecurityEnabled = deviceHasDeviceSecurityEnabled, + deviceHasBiometryEnabled = deviceHasBiometryEnabled, + lazyListState = lazyListState, + onPasswordChange = onPasswordChange, + onRepeatedPasswordChange = onRepeatedPasswordChange, + onTabChange = onTabChange, + onChoosePassword = onChoosePassword + ) + } - if (BuildConfigExtension.isNonReleaseMode) { - SkipOnBoardingButton { - controller.createProfileOnSkipOnboarding() - navController.finishOnboardingAsSuccessAndOpenPrescriptions() - } - } + if (BuildConfigExtension.isNonReleaseMode) { + SkipOnBoardingButton(onSkip) } } +@Requirement( + "O.Resi_1#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Selection of secure app login method in onboarding process" +) private fun LazyListScope.onboardingSelectAppLoginContent( selectedTab: OnboardingAuthTab, onTabChange: (Int) -> Unit, - secureMethod: OnboardingSecureAppMethod, + passwordFieldsState: PasswordFieldsData, + deviceHasDeviceSecurityEnabled: Boolean, + deviceHasBiometryEnabled: Boolean, lazyListState: LazyListState, - onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit, - onNext: () -> Unit + onPasswordChange: (String) -> Unit, + onRepeatedPasswordChange: (String) -> Unit, + onChoosePassword: () -> Unit ) { item { Image( @@ -201,7 +271,11 @@ private fun LazyListScope.onboardingSelectAppLoginContent( backGroundColor = MaterialTheme.colors.background, onClick = onTabChange, tabs = listOf( - stringResource(R.string.onboarding_secure_app_biometric), + if (!deviceHasBiometryEnabled && deviceHasDeviceSecurityEnabled) { stringResource(id = R.string.settings_app_security_device_security) } else { + stringResource( + R.string.onboarding_secure_app_biometric + ) + }, stringResource(R.string.onboarding_secure_app_password) ), testTags = listOf( @@ -225,10 +299,11 @@ private fun LazyListScope.onboardingSelectAppLoginContent( when (targetTab) { OnboardingAuthTab.Password -> { PasswordAuthentication( - secureMethod = secureMethod, + passwordFieldsState = passwordFieldsState, lazyListState = lazyListState, - onSecureMethodChange = onSecureMethodChange, - onNext = onNext + onPasswordChange = onPasswordChange, + onRepeatedPasswordChange = onRepeatedPasswordChange, + onNext = onChoosePassword ) } @@ -243,25 +318,15 @@ private fun LazyListScope.onboardingSelectAppLoginContent( @Composable private fun PasswordAuthentication( - secureMethod: OnboardingSecureAppMethod, + passwordFieldsState: PasswordFieldsData, lazyListState: LazyListState, - onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit, + onPasswordChange: (String) -> Unit, + onRepeatedPasswordChange: (String) -> Unit, onNext: () -> Unit ) { var offsetFirstPassword by remember { mutableIntStateOf(0) } var offsetSecondPassword by remember { mutableIntStateOf(0) } - val password = - remember(secureMethod) { (secureMethod as? Password)?.password ?: "" } - val repeatedPassword = - remember(secureMethod) { - (secureMethod as? Password)?.repeatedPassword ?: "" - } - val passwordScore = - remember(secureMethod) { - (secureMethod as? Password)?.score ?: 0 - } - val focusManager = LocalFocusManager.current Column( @@ -274,46 +339,35 @@ private fun PasswordAuthentication( .scrollOnFocus(POS_OF_ANIMATED_CONTENT_ITEM, lazyListState, offsetFirstPassword) .onGloballyPositioned { offsetFirstPassword = it.positionInParent().y.toInt() } .padding(bottom = PaddingDefaults.Tiny), - value = password, - onValueChange = { - if (it.isEmpty()) { - onSecureMethodChange(None) - } else { - onSecureMethodChange( - Password( - password = it, - repeatedPassword = repeatedPassword, - score = passwordScore - ) - ) - } - }, + value = passwordFieldsState.password, + onValueChange = onPasswordChange, onSubmit = { - if (validatePasswordScore(passwordScore)) { + if (passwordFieldsState.passwordEvaluation.isStrongEnough) { focusManager.moveFocus(FocusDirection.Down) } }, allowAutofill = true, allowVisiblePassword = true, label = { - Text(stringResource(R.string.settings_password_enter)) + Text(stringResource(R.string.settings_password_entry)) } ) + @Requirement( + "O.Pass_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Usage of password strength evaluation to ensure a secure password for onboarding" + ) + @Requirement( + "O.Pass_2#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Shows password strength within the onboarding process" + ) PasswordStrength( modifier = Modifier .testTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) .fillMaxWidth() .padding(bottom = PaddingDefaults.Medium), - password = password, - onScoreChange = { - onSecureMethodChange( - Password( - password = password, - repeatedPassword = repeatedPassword, - score = it - ) - ) - } + passwordEvaluation = passwordFieldsState.passwordEvaluation ) ConfirmationPasswordTextField( modifier = Modifier @@ -321,24 +375,16 @@ private fun PasswordAuthentication( .fillMaxWidth() .scrollOnFocus(POS_OF_ANIMATED_CONTENT_ITEM, lazyListState, offsetSecondPassword) .onGloballyPositioned { offsetSecondPassword = it.positionInParent().y.toInt() }, - password = password, - value = repeatedPassword, - passwordScore = passwordScore, - onValueChange = { - onSecureMethodChange( - Password( - password = password, - repeatedPassword = it, - score = passwordScore - ) - ) - }, + value = passwordFieldsState.repeatedPassword, + onValueChange = onRepeatedPasswordChange, + repeatedPasswordHasError = passwordFieldsState.repeatedPasswordHasError, + passwordIsValidAndConsistent = passwordFieldsState.passwordIsValidAndConsistent, onSubmit = { focusManager.clearFocus() onNext() } ) - if (repeatedPassword.isNotBlank() && repeatedPassword != password) { + if (passwordFieldsState.repeatedPasswordHasError) { SpacerTiny() Text( stringResource(R.string.not_matching_entries), @@ -351,54 +397,24 @@ private fun PasswordAuthentication( @LightDarkPreview @Composable -fun PasswordAuthenticationSuccessfulPreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - PasswordAuthentication( - secureMethod = Password( - "azerbaijan@89Atropates", - "azerbaijan@89Atropates", - SUCCESS_SCORE - ), - lazyListState = lazyListState, - onSecureMethodChange = {}, - onNext = {} - ) - } -} - -@LightDarkPreview -@Composable -fun PasswordAuthenticationFailurePreview() { - val lazyListState = rememberLazyListState() - PreviewAppTheme { - PasswordAuthentication( - secureMethod = Password( - "azerbaijan@89", - "", - MEDIOCRE_SCORE - ), - lazyListState = lazyListState, - onSecureMethodChange = {}, - onNext = {} - ) - } -} - -@LightDarkPreview -@Composable -fun PasswordAuthenticationEmptyPreview() { +fun PasswordAuthenticationPreview( + @PreviewParameter(SetAppPasswordParameterProvider::class) parameter: SetAppPasswordParameter +) { val lazyListState = rememberLazyListState() PreviewAppTheme { - PasswordAuthentication( - secureMethod = Password( - "", - "", - FAILURE_SCORE - ), + OnboardingScreenContent( + selectedTab = OnboardingAuthTab.Password, + passwordFieldsState = parameter.passwordFieldsState, lazyListState = lazyListState, - onSecureMethodChange = {}, - onNext = {} + deviceSupportsDeviceSecurity = false, + deviceHasBiometryEnabled = false, + deviceHasDeviceSecurityEnabled = false, + onPasswordChange = {}, + onRepeatedPasswordChange = {}, + onTabChange = {}, + onChoosePassword = {}, + onChooseDeviceSecurity = {}, + onSkip = {} ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreen.kt index aba26431..0893d4b0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -55,11 +55,11 @@ import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.navigation.navigateAndClearStack import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes.OnboardingDataProtectionAndTermsOfUseOverviewScreen import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension.isDebugOrMinifiedDebug import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension.isNonReleaseMode import kotlinx.coroutines.Job @@ -74,12 +74,11 @@ private const val FLAG_PADDING = 10 // Should be replaced by a splash screen and come as the first one in the app class OnboardingWelcomeScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { @Composable override fun Content() { - val controller = rememberOnboardingController() - var job: Job? by remember { mutableStateOf(null) } DelayedNavigation( @@ -93,7 +92,7 @@ class OnboardingWelcomeScreen( // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots // (on debug builds should be allowed for testing) if (isDebugOrMinifiedDebug) { - controller.allowScreenshots(true) + graphController.allowScreenshots(true) } } @@ -102,7 +101,7 @@ class OnboardingWelcomeScreen( if (isNonReleaseMode) { SkipOnBoardingButton { job?.cancel() - controller.createProfileOnSkipOnboarding() + graphController.createProfileOnSkipOnboarding() navController.finishOnboardingAsSuccessAndOpenPrescriptions() } } @@ -130,7 +129,7 @@ class OnboardingWelcomeScreen( } @Composable -private fun OnboardingWelcomeScreenContent() { +internal fun OnboardingWelcomeScreenContent() { Surface { Column( modifier = Modifier diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/SkipOnBoardingButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/SkipOnBoardingButton.kt index 3382489b..f6ad977a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/SkipOnBoardingButton.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/SkipOnBoardingButton.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable fun SkipOnBoardingButton(onClick: () -> Unit) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/TermsOfUseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/TermsOfUseScreen.kt index a0b7a585..cd8915ee 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/TermsOfUseScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/TermsOfUseScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.onboarding.ui @@ -29,28 +29,29 @@ import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.onboarding.navigation.finishOnboardingAsSuccessAndOpenPrescriptions -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController +import de.gematik.ti.erp.app.onboarding.presentation.OnboardingGraphController import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE import de.gematik.ti.erp.app.webview.WebViewScreen @Requirement( - "O.Purp_1#1", "O.Arch_8#5", - "O.Plat_11#5", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display terms of use as part of the onboarding. " + - "Webview containing local html without javascript" + rationale = "Webview containing local html without javascript" +) +@Requirement( + "O.Purp_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display terms of use as part of the onboarding. " ) class TermsOfUseScreen( override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnboardingGraphController ) : Screen() { @Composable override fun Content() { - val controller = rememberOnboardingController() - WebViewScreen( modifier = Modifier.testTag(TestTag.Onboarding.TermsOfUseScreen), title = stringResource(R.string.onb_terms_of_use), @@ -59,7 +60,7 @@ class TermsOfUseScreen( ) if (BuildConfigExtension.isNonReleaseMode) { SkipOnBoardingButton { - controller.createProfileOnSkipOnboarding() + graphController.createProfileOnSkipOnboarding() navController.finishOnboardingAsSuccessAndOpenPrescriptions() } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt deleted file mode 100644 index 27a560c3..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard - -import de.gematik.ti.erp.app.orderhealthcard.usecase.HealthCardOrderUseCase -import org.kodein.di.DI -import org.kodein.di.bindProvider -import org.kodein.di.instance - -val orderHealthCardModule = DI.Module("orderHealthCardModule") { - bindProvider { HealthCardOrderUseCase(instance()) } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/di/OrderHealthCardModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/di/OrderHealthCardModule.kt new file mode 100644 index 00000000..b4f09a8f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/di/OrderHealthCardModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.di + +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController +import de.gematik.ti.erp.app.orderhealthcard.usecase.LoadHealthInsuranceListUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val orderHealthCardModule = DI.Module("orderHealthCardModule") { + bindProvider { LoadHealthInsuranceListUseCase(instance()) } + bindSingleton { OrderHealthCardGraphController() } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardGraph.kt new file mode 100644 index 00000000..fd70483a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardGraph.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectInsuranceCompanyScreen +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectMethodScreen +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectOptionScreen +import org.kodein.di.DI +import org.kodein.di.instance + +fun NavGraphBuilder.orderHealthCardGraph( + dependencyInjector: DI, + startDestination: String = OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.route, + navController: NavController +) { + val controller by dependencyInjector.instance() + + navigation( + startDestination = startDestination, + route = OrderHealthCardRoutes.subGraphName() + ) { + renderComposable( + route = OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.route + ) { navEntry -> + OrderHealthCardSelectInsuranceCompanyScreen( + navController = navController, + navBackStackEntry = navEntry, + graphController = controller + ) + } + renderComposable( + route = OrderHealthCardRoutes.OrderHealthCardSelectMethodScreen.route + ) { navEntry -> + OrderHealthCardSelectMethodScreen( + navController = navController, + navBackStackEntry = navEntry, + graphController = controller + ) + } + renderComposable( + route = OrderHealthCardRoutes.OrderHealthCardSelectOptionScreen.route + ) { navEntry -> + OrderHealthCardSelectOptionScreen( + navController = navController, + navBackStackEntry = navEntry, + graphController = controller + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardRoutes.kt new file mode 100644 index 00000000..764e03b9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardRoutes.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.navigation + +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes + +object OrderHealthCardRoutes : NavigationRoutes { + override fun subGraphName() = "orderHealthCard" + object OrderHealthCardSelectInsuranceCompanyScreen : Routes( + NavigationRouteNames.OrderHealthCardSelectInsuranceCompanyScreen.name + ) + object OrderHealthCardSelectOptionScreen : Routes( + NavigationRouteNames.OrderHealthCardSelectOptionScreen.name + ) + object OrderHealthCardSelectMethodScreen : Routes( + NavigationRouteNames.OrderHealthCardSelectMethodScreen.name + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardScreen.kt new file mode 100644 index 00000000..6377a9ba --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/navigation/OrderHealthCardScreen.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.navigation + +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController + +abstract class OrderHealthCardScreen : Screen() { + abstract val graphController: OrderHealthCardGraphController +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/HealthInsuranceCompany.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/HealthInsuranceCompany.kt new file mode 100644 index 00000000..3aa4043c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/HealthInsuranceCompany.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.presentation + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +data class HealthInsuranceCompany( + val name: String, + val healthCardAndPinPhone: String?, + val healthCardAndPinMail: String?, + val healthCardAndPinUrl: String?, + val pinUrl: String?, + val subjectCardAndPinMail: String?, + val bodyCardAndPinMail: String?, + val subjectPinMail: String?, + val bodyPinMail: String? +) { + fun noContactInformation() = + healthCardAndPinPhone.isNullOrEmpty() && + healthCardAndPinMail.isNullOrEmpty() && + healthCardAndPinUrl.isNullOrEmpty() && + pinUrl.isNullOrEmpty() + + fun singleContactInformation() = + ( + !healthCardAndPinPhone.isNullOrEmpty() && + healthCardAndPinMail.isNullOrEmpty() && + healthCardAndPinUrl.isNullOrEmpty() && + pinUrl.isNullOrEmpty() + ) || + ( + healthCardAndPinPhone.isNullOrEmpty() && + !healthCardAndPinMail.isNullOrEmpty() && + healthCardAndPinUrl.isNullOrEmpty() && + pinUrl.isNullOrEmpty() + ) || + ( + healthCardAndPinPhone.isNullOrEmpty() && + healthCardAndPinMail.isNullOrEmpty() && + !healthCardAndPinUrl.isNullOrEmpty() && + !pinUrl.isNullOrEmpty() + ) + + fun hasContactInfoForPin() = + !pinUrl.isNullOrEmpty() || ( + !healthCardAndPinMail.isNullOrEmpty() && + !bodyPinMail.isNullOrEmpty() && !subjectPinMail.isNullOrEmpty() + ) + + fun hasContactInfoForHealthCardAndPin() = + !healthCardAndPinPhone.isNullOrEmpty() || + !healthCardAndPinMail.isNullOrEmpty() || + !healthCardAndPinUrl.isNullOrEmpty() + + fun hasMailContentForCardAndPin() = !subjectCardAndPinMail.isNullOrEmpty() && + !bodyCardAndPinMail.isNullOrEmpty() + + fun hasMailContentForPin() = !subjectPinMail.isNullOrEmpty() && !bodyPinMail.isNullOrEmpty() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardGraphController.kt new file mode 100644 index 00000000..d29facd1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardGraphController.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.presentation + +import de.gematik.ti.erp.app.base.Controller +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class OrderHealthCardGraphController : Controller() { + private var _selectedInsuranceCompany: MutableStateFlow = + MutableStateFlow(null) + private var _selectedContactOption = MutableStateFlow(OrderHealthCardContactOption.NotChosen) + + val selectedInsuranceCompany: StateFlow = _selectedInsuranceCompany + val selectedContactOption: StateFlow = _selectedContactOption + + init { + reset() + } + + fun reset() { + controllerScope.launch { + _selectedInsuranceCompany.value = null + _selectedContactOption.value = OrderHealthCardContactOption.NotChosen + } + } + + fun setInsuranceCompany(company: HealthInsuranceCompany) { + _selectedInsuranceCompany.value = company + } + + fun setContactOption(option: OrderHealthCardContactOption) { + _selectedContactOption.value = option + } +} + +enum class OrderHealthCardContactOption { + WithHealthCardAndPin, PinOnly, NotChosen +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardHealthInsuranceListController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardHealthInsuranceListController.kt new file mode 100644 index 00000000..01332a98 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/presentation/OrderHealthCardHealthInsuranceListController.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.orderhealthcard.usecase.LoadHealthInsuranceListUseCase +import org.kodein.di.compose.rememberInstance + +class OrderHealthCardHealthInsuranceListController( + loadHealthInsuranceListUseCase: LoadHealthInsuranceListUseCase +) { + private val _healthInsuranceList = loadHealthInsuranceListUseCase() + val healthInsuranceList + @Composable + get() = _healthInsuranceList.collectAsStateWithLifecycle( + emptyList() + ) +} + +@Composable +fun rememberHealthInsuranceListController(): OrderHealthCardHealthInsuranceListController { + val loadHealthInsuranceListUseCase by rememberInstance() + return remember { + OrderHealthCardHealthInsuranceListController( + loadHealthInsuranceListUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt deleted file mode 100644 index 958c4717..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard.ui - -import android.net.Uri -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Card -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MailOutline -import androidx.compose.material.icons.filled.OpenInBrowser -import androidx.compose.material.icons.filled.PhoneInTalk -import androidx.compose.material.icons.outlined.KeyboardArrowRight -import androidx.compose.material.icons.rounded.Search -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationAnimation -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.navigationModeState -import de.gematik.ti.erp.app.utils.openMailClient - -@Composable -fun HealthCardContactOrderScreen( - onBack: () -> Unit -) { - val healthCardOrderState = rememberHealthCardOrderState() - val state by healthCardOrderState.state - - val navController = rememberNavController() - var previousNavEntry by remember { mutableStateOf("contactInsuranceCompany") } - trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - val title = stringResource(R.string.health_insurance_search_page_title) - - val navigationMode by navController.navigationModeState(HealthCardOrderNavigationScreens.HealthCardOrder.route) - NavHost( - navController, - startDestination = HealthCardOrderNavigationScreens.HealthCardOrder.path() - ) { - composable(HealthCardOrderNavigationScreens.HealthCardOrder.route) { - val listState = rememberLazyListState() - - NavigationAnimation(mode = navigationMode) { - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Settings.OrderEgk.OrderEgkScreen), - topBarTitle = title, - elevated = listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0, - navigationMode = NavigationBarMode.Back, - onBack = onBack, - actions = {} - ) { - HealthCardOrder( - listState = listState, - healthCardOrderState = state, - onSelectCompany = { - healthCardOrderState.onSelectInsuranceCompany(it) - when (true) { - (it.hasContactInfoForHealthCardAndPin() && it.hasContactInfoForPin()) -> - navController.navigate(HealthCardOrderNavigationScreens.SelectOrderOption.path()) - it.hasContactInfoForHealthCardAndPin() -> { - healthCardOrderState.onSelectContactOption( - HealthCardOrderStateData.ContactInsuranceOption.WithHealthCardAndPin - ) - navController.navigate( - HealthCardOrderNavigationScreens.HealthCardOrderContact.path() - ) - } - it.hasContactInfoForPin() -> { - healthCardOrderState.onSelectContactOption( - HealthCardOrderStateData.ContactInsuranceOption.PinOnly - ) - navController.navigate( - HealthCardOrderNavigationScreens.HealthCardOrderContact.path() - ) - } - else -> { - navController.navigate( - HealthCardOrderNavigationScreens.HealthCardOrderContact.path() - ) - } - } - } - ) - } - } - } - - composable(HealthCardOrderNavigationScreens.SelectOrderOption.route) { - val listState = rememberLazyListState() - - NavigationAnimation(mode = navigationMode) { - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Settings.OrderEgk.SelectOrderOptionScreen), - topBarTitle = "", - elevated = listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0, - navigationMode = NavigationBarMode.Back, - onBack = { navController.popBackStack() }, - actions = { - TextButton(onClick = onBack) { - Text(stringResource(R.string.health_card_order_cancel)) - } - } - ) { - SelectOrderOption( - listState = listState, - onSelectOption = { - healthCardOrderState.onSelectContactOption(it) - navController.navigate(HealthCardOrderNavigationScreens.HealthCardOrderContact.path()) - } - ) - } - } - } - composable(HealthCardOrderNavigationScreens.HealthCardOrderContact.route) { - val listState = rememberLazyListState() - - NavigationAnimation(mode = navigationMode) { - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Settings.OrderEgk.HealthCardOrderContactScreen), - topBarTitle = "", - elevated = listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0, - navigationMode = NavigationBarMode.Back, - onBack = { navController.popBackStack() }, - actions = { - TextButton(onClick = onBack) { - Text(stringResource(R.string.health_card_order_close)) - } - } - ) { - ContactInsurance( - listState, - state - ) - } - } - } - } -} - -@Composable -private fun HealthInsuranceCompanySelectable( - company: HealthCardOrderUseCaseData.HealthInsuranceCompany, - onSelectionCompany: (HealthCardOrderUseCaseData.HealthInsuranceCompany) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { onSelectionCompany(company) }) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - .testTag(TestTag.Settings.InsuranceCompanyList.ListOfInsuranceButtons), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - company.name, - style = AppTheme.typography.body1, - modifier = Modifier.weight(1f) - ) - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } -} - -@Composable -private fun HealthCardOrder( - listState: LazyListState = rememberLazyListState(), - healthCardOrderState: HealthCardOrderStateData.HealthCardOrderState, - onSelectCompany: (HealthCardOrderUseCaseData.HealthInsuranceCompany) -> Unit -) { - var searchName by remember { - mutableStateOf("") - } - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .testTag(TestTag.Settings.InsuranceCompanyList.InsuranceSelectionContent), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - SpacerMedium() - InsuranceCompanySearchField(modifier = Modifier, searchValue = searchName, onSearchChange = { - searchName = it.trim() - }) - SpacerMedium() - } - items( - healthCardOrderState.companies.filter { - it.name.contains(searchName, true) - } - ) { - HealthInsuranceCompanySelectable(it, onSelectCompany) - } - } -} - -@Composable -private fun InsuranceCompanySearchField( - modifier: Modifier, - searchValue: String, - onSearchChange: (String) -> Unit -) { - TextField( - value = searchValue, - placeholder = { Text(stringResource(R.string.health_card_search_field_place_holder)) }, - onValueChange = onSearchChange, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - singleLine = true, - keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Search - ), - keyboardActions = KeyboardActions { - onSearchChange(searchValue) - }, - visualTransformation = VisualTransformation.None, - leadingIcon = { - IconButton( - onClick = { onSearchChange(searchValue) } - ) { - Icon( - Icons.Rounded.Search, - contentDescription = null - ) - } - }, - shape = RoundedCornerShape(16.dp), - textStyle = AppTheme.typography.body1, - colors = TextFieldDefaults.textFieldColors( - textColor = AppTheme.colors.neutral900, - leadingIconColor = AppTheme.colors.neutral600, - trailingIconColor = AppTheme.colors.neutral600, - backgroundColor = AppTheme.colors.neutral100, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ) - ) -} - -@Composable -private fun SelectOrderOption( - listState: LazyListState = rememberLazyListState(), - onSelectOption: (HealthCardOrderStateData.ContactInsuranceOption) -> Unit -) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .testTag(TestTag.Settings.OrderEgk.SelectOrderOptionContent) - .padding(horizontal = PaddingDefaults.Medium), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - SpacerXXLarge() - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - Image(painter = painterResource(R.drawable.egk_on_blue_circle), contentDescription = null) - } - } - item { - SpacerXXLarge() - SpacerXXLarge() - Text( - stringResource(id = R.string.select_order_option_header), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - } - item { - SpacerSmall() - Text( - stringResource(id = R.string.select_order_option_info), - style = AppTheme.typography.subtitle2, - textAlign = TextAlign.Center - ) - SpacerXXLarge() - } - item { - Option( - testTag = TestTag.Settings.ContactInsuranceCompany.OrderPinButton, - name = stringResource(R.string.cdw_health_insurance_contact_pin_only), - onSelect = { onSelectOption(HealthCardOrderStateData.ContactInsuranceOption.PinOnly) } - ) - SpacerMedium() - } - item { - Option( - testTag = TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton, - name = stringResource(R.string.cdw_health_insurance_contact_healthcard_pin), - onSelect = { onSelectOption(HealthCardOrderStateData.ContactInsuranceOption.WithHealthCardAndPin) } - ) - } - } -} - -@Composable -private fun Option( - name: String, - testTag: String, - onSelect: () -> Unit -) { - val shape = RoundedCornerShape(16.dp) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .testTag(testTag) - .clip(shape) - .border(1.dp, color = AppTheme.colors.neutral300, shape = shape) - .clickable(onClick = onSelect) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Text(name, style = AppTheme.typography.body1, modifier = Modifier.weight(1f)) - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } -} - -@Composable -private fun ContactInsurance( - listState: LazyListState = rememberLazyListState(), - healthCardOrderState: HealthCardOrderStateData.HealthCardOrderState -) { - healthCardOrderState.selectedCompany?.let { - if (healthCardOrderState.selectedCompany.noContactInformation()) { - NoContactInformation() - } else { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .testTag(TestTag.Settings.OrderEgk.HealthCardOrderContactScreenContent) - .padding(horizontal = PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally, - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - val header = stringResource(R.string.order_health_card_contact_header) - val info = if (healthCardOrderState.selectedCompany.singleContactInformation()) { - stringResource(R.string.order_health_card_contact_info_single) - } else { - stringResource(R.string.order_health_card_contact_info) - } - SpacerMedium() - Text( - text = header, - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - text = info, - style = AppTheme.typography.subtitle2, - color = AppTheme.colors.neutral600, - textAlign = TextAlign.Center - ) - SpacerXXLarge() - } - - when (healthCardOrderState.selectedOption) { - HealthCardOrderStateData.ContactInsuranceOption.WithHealthCardAndPin -> - item { - ContactMethodRow( - phone = it.healthCardAndPinPhone, - url = it.healthCardAndPinUrl, - mail = it.healthCardAndPinMail, - company = it, - option = healthCardOrderState.selectedOption - ) - } - - else -> - item { - ContactMethodRow( - phone = it.healthCardAndPinPhone, - url = it.pinUrl, - mail = if (!it.subjectPinMail.isNullOrEmpty()) it.healthCardAndPinMail else null, - company = it, - option = healthCardOrderState.selectedOption - ) - } - } - } - } - } -} - -@Composable -private fun NoContactInformation() { - Column( - modifier = Modifier.fillMaxSize().padding(PaddingDefaults.Medium), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(R.drawable.illustration_girl), - contentDescription = null, - alignment = Alignment.Center - ) - - Text( - stringResource(R.string.cdw_health_insurance_no_cantacts_title), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - - SpacerSmall() - Text( - stringResource(R.string.cdw_health_insurance_no_cantacts_body), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun ContactMethodRow( - phone: String?, - url: String?, - mail: String?, - company: HealthCardOrderUseCaseData.HealthInsuranceCompany, - option: HealthCardOrderStateData.ContactInsuranceOption -) { - val uriHandler = LocalUriHandler.current - - val mailSubject = stringResource(R.string.cdw_health_insurance_mail_subject) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - phone?.let { - ContactMethod( - modifier = Modifier - .testTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton), - name = stringResource(R.string.healthcard_order_phone), - icon = Icons.Filled.PhoneInTalk, - onClick = { - uriHandler.openUri("tel:$phone") - } - ) - } - url?.let { - ContactMethod( - modifier = Modifier - .testTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton), - name = stringResource(R.string.healthcard_order_website), - icon = Icons.Filled.OpenInBrowser, - onClick = { - uriHandler.openUri(url) - } - ) - } - mail?.let { - val context = LocalContext.current - ContactMethod( - modifier = Modifier - .testTag(TestTag.Settings.ContactInsuranceCompany.MailToButton), - name = stringResource(R.string.healthcard_order_mail), - icon = Icons.Filled.MailOutline, - onClick = { - when { - option == HealthCardOrderStateData.ContactInsuranceOption.WithHealthCardAndPin && - company.hasMailContentForCardAndPin() -> openMailClient( - context = context, - address = mail, - subject = company.subjectCardAndPinMail!!, - body = company.bodyCardAndPinMail!! - ) - - option == HealthCardOrderStateData.ContactInsuranceOption.PinOnly && - company.hasMailContentForPin() -> openMailClient( - context = context, - address = mail, - subject = company.subjectPinMail!!, - body = company.bodyPinMail!! - ) - - else -> uriHandler.openUri("mailto:$mail?subject=${Uri.encode(mailSubject)}") - } - } - ) - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun ContactMethod( - modifier: Modifier, - name: String, - icon: ImageVector, - onClick: () -> Unit -) { - Card( - modifier = modifier, - onClick = onClick, - shape = RoundedCornerShape(8.dp), - contentColor = AppTheme.colors.primary600, - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - backgroundColor = AppTheme.colors.neutral050, - elevation = 0.dp - ) { - Column(Modifier.padding(PaddingDefaults.Medium), horizontalAlignment = Alignment.CenterHorizontally) { - Icon(icon, null) - SpacerSmall() - Text( - name, - modifier = modifier.widthIn(72.dp), - textAlign = TextAlign.Center, - style = AppTheme.typography.subtitle2 - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt deleted file mode 100644 index e06a352e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.remember -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.navigation.Routes -import de.gematik.ti.erp.app.orderhealthcard.usecase.HealthCardOrderUseCase -import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import org.kodein.di.compose.rememberInstance - -object HealthCardOrderNavigationScreens { - object HealthCardOrder : Routes("contactInsuranceCompany") - object SelectOrderOption : Routes("contactInsuranceCompany_selectReason") - object HealthCardOrderContact : Routes("contactInsuranceCompany_selectMethod") -} - -class HealthCardOrderState( - healthCardOrderUseCase: HealthCardOrderUseCase -) { - - private var selectedCompanyFlow: MutableStateFlow = - MutableStateFlow(null) - - private var selectedOptionFlow = MutableStateFlow(HealthCardOrderStateData.ContactInsuranceOption.NotChosen) - - private var healthCardOrderStateFlow = combine( - healthCardOrderUseCase.healthInsuranceOrderContacts, - selectedCompanyFlow, - selectedOptionFlow - ) { - companies, company, option -> - HealthCardOrderStateData.HealthCardOrderState(companies, company, option) - } - - val state - @Composable - get() = healthCardOrderStateFlow.collectAsStateWithLifecycle( - HealthCardOrderStateData.defaultHealthCardOrderState - ) - - fun onSelectInsuranceCompany(company: HealthCardOrderUseCaseData.HealthInsuranceCompany) { - selectedCompanyFlow.value = company - } - - fun onSelectContactOption(option: HealthCardOrderStateData.ContactInsuranceOption) { - selectedOptionFlow.value = option - } -} - -@Composable -fun rememberHealthCardOrderState(): HealthCardOrderState { - val healthCardOrderUseCase by rememberInstance() - return remember { - HealthCardOrderState( - healthCardOrderUseCase - ) - } -} - -object HealthCardOrderStateData { - @Immutable - data class HealthCardOrderState( - val companies: List, - val selectedCompany: HealthCardOrderUseCaseData.HealthInsuranceCompany?, - val selectedOption: ContactInsuranceOption - ) - - val defaultHealthCardOrderState = HealthCardOrderState( - companies = emptyList(), - selectedCompany = null, - selectedOption = ContactInsuranceOption.NotChosen - ) - - enum class ContactInsuranceOption { - WithHealthCardAndPin, PinOnly, NotChosen - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectInsuranceCompanyScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectInsuranceCompanyScreen.kt new file mode 100644 index 00000000..068b5d09 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectInsuranceCompanyScreen.kt @@ -0,0 +1,290 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.KeyboardArrowRight +import androidx.compose.material.icons.rounded.Search +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardScreen +import de.gematik.ti.erp.app.orderhealthcard.presentation.HealthInsuranceCompany +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardContactOption +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController +import de.gematik.ti.erp.app.orderhealthcard.presentation.rememberHealthInsuranceListController +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.HealthInsuranceSearchData +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.OrderHealthCardPreviewParameter +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class OrderHealthCardSelectInsuranceCompanyScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: OrderHealthCardGraphController +) : OrderHealthCardScreen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + val healthInsuranceListController = rememberHealthInsuranceListController() + val healthInsuranceList by healthInsuranceListController.healthInsuranceList + + var searchName by remember { mutableStateOf("") } + val filteredList = remember(searchName, healthInsuranceList) { + healthInsuranceList.filter { it.name.contains(searchName, true) } + } + + OrderHealthCardSelectInsuranceCompanyScreenScaffold( + listState = listState, + onBack = { navController.popBackStack() }, + healthInsuranceList = filteredList, + searchName = searchName, + onSearchChange = { searchName = it }, + onSelectCompany = { + graphController.setInsuranceCompany(it) + when { + (it.hasContactInfoForHealthCardAndPin() && it.hasContactInfoForPin()) -> + navController.navigate(OrderHealthCardRoutes.OrderHealthCardSelectOptionScreen.path()) + it.hasContactInfoForHealthCardAndPin() -> { + graphController.setContactOption( + OrderHealthCardContactOption.WithHealthCardAndPin + ) + navController.navigate( + OrderHealthCardRoutes.OrderHealthCardSelectMethodScreen.path() + ) + } + it.hasContactInfoForPin() -> { + graphController.setContactOption( + OrderHealthCardContactOption.PinOnly + ) + navController.navigate( + OrderHealthCardRoutes.OrderHealthCardSelectMethodScreen.path() + ) + } + else -> { + navController.navigate( + OrderHealthCardRoutes.OrderHealthCardSelectMethodScreen.path() + ) + } + } + } + ) + } +} + +@Composable +private fun OrderHealthCardSelectInsuranceCompanyScreenScaffold( + listState: LazyListState, + healthInsuranceList: List, + searchName: String, + onSearchChange: (String) -> Unit, + onBack: () -> Unit, + onSelectCompany: (HealthInsuranceCompany) -> Unit +) { + AnimatedElevationScaffold( + listState = listState, + modifier = Modifier.testTag(TestTag.Settings.OrderEgk.OrderEgkScreen), + topBarTitle = stringResource(R.string.health_insurance_search_page_title), + navigationMode = NavigationBarMode.Back, + onBack = onBack, + actions = {} + ) { + OrderHealthCardSelectInsuranceCompanyScreenContent( + listState = listState, + healthInsuranceList = healthInsuranceList, + searchName = searchName, + onSearchChange = onSearchChange, + onSelectCompany = onSelectCompany + ) + } +} + +@Composable +private fun OrderHealthCardSelectInsuranceCompanyScreenContent( + listState: LazyListState, + healthInsuranceList: List, + searchName: String, + onSearchChange: (String) -> Unit, + onSelectCompany: (HealthInsuranceCompany) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Settings.InsuranceCompanyList.InsuranceSelectionContent), + state = listState, + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { + item { + SpacerMedium() + InsuranceCompanySearchField( + modifier = Modifier, + searchValue = searchName, + onSearchChange = onSearchChange + ) + SpacerMedium() + } + items( + healthInsuranceList.size + ) { idx -> + HealthInsuranceCompanySelectable(healthInsuranceList[idx], onSelectCompany) + } + } +} + +@Composable +private fun InsuranceCompanySearchField( + modifier: Modifier, + searchValue: String, + onSearchChange: (String) -> Unit +) { + TextField( + value = searchValue, + placeholder = { Text(stringResource(R.string.health_card_search_field_place_holder)) }, + onValueChange = onSearchChange, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + singleLine = true, + keyboardOptions = KeyboardOptions( + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions { + onSearchChange(searchValue) + }, + visualTransformation = VisualTransformation.None, + leadingIcon = { + IconButton( + onClick = { onSearchChange(searchValue) } + ) { + Icon( + Icons.Rounded.Search, + contentDescription = null + ) + } + }, + shape = RoundedCornerShape(SizeDefaults.double), + textStyle = AppTheme.typography.body1, + colors = TextFieldDefaults.textFieldColors( + textColor = AppTheme.colors.neutral900, + leadingIconColor = AppTheme.colors.neutral600, + trailingIconColor = AppTheme.colors.neutral600, + backgroundColor = AppTheme.colors.neutral100, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) +} + +@Composable +private fun HealthInsuranceCompanySelectable( + company: HealthInsuranceCompany, + onSelectionCompany: (HealthInsuranceCompany) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { onSelectionCompany(company) }) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + .testTag(TestTag.Settings.InsuranceCompanyList.ListOfInsuranceButtons), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + company.name, + style = AppTheme.typography.body1, + modifier = Modifier.weight(1f) + ) + Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@LightDarkPreview +@Composable +fun OrderHealthCardSelectInsuranceCompanyScreenPreview( + @PreviewParameter(OrderHealthCardPreviewParameter::class) previewData: HealthInsuranceSearchData +) { + val listState = rememberLazyListState() + PreviewAppTheme { + var searchName by remember { mutableStateOf(previewData.searchText) } + val healthInsuranceList = listOf( + previewData.healthInsurance, + previewData.healthInsurance.copy(name = "Insurance Company A"), + previewData.healthInsurance.copy(name = "Insurance Company B"), + previewData.healthInsurance.copy(name = "Insurance Company C"), + previewData.healthInsurance.copy(name = "Insurance Company D"), + previewData.healthInsurance.copy(name = "Insurance Company E"), + previewData.healthInsurance.copy(name = "Insurance Company F"), + previewData.healthInsurance.copy(name = "Insurance Company G") + ).filter { it.name.contains(searchName, true) } + + OrderHealthCardSelectInsuranceCompanyScreenScaffold( + listState = listState, + onBack = {}, + onSelectCompany = {}, + healthInsuranceList = healthInsuranceList, + searchName = searchName, + onSearchChange = { searchName = it } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectMethodScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectMethodScreen.kt new file mode 100644 index 00000000..af71d650 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectMethodScreen.kt @@ -0,0 +1,352 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui + +import android.net.Uri +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MailOutline +import androidx.compose.material.icons.filled.OpenInBrowser +import androidx.compose.material.icons.filled.PhoneInTalk +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardScreen +import de.gematik.ti.erp.app.orderhealthcard.presentation.HealthInsuranceCompany +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardContactOption +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.OrderHealthCardPreviewData.healthInsuranceCompany +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid +import de.gematik.ti.erp.app.utils.openMailClient + +class OrderHealthCardSelectMethodScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: OrderHealthCardGraphController +) : OrderHealthCardScreen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + val selectedCompany by graphController.selectedInsuranceCompany.collectAsStateWithLifecycle() + val selectedOption by graphController.selectedContactOption.collectAsStateWithLifecycle() + OrderHealthCardSelectMethodScreenScaffold( + listState, + selectedCompany, + selectedOption, + onBack = { navController.popBackStack() }, + onClose = { + navController.popBackStack( + OrderHealthCardRoutes.subGraphName(), + inclusive = true + ) + } + ) + } +} + +@Composable +private fun OrderHealthCardSelectMethodScreenScaffold( + listState: LazyListState, + selectedCompany: HealthInsuranceCompany?, + selectedOption: OrderHealthCardContactOption, + onBack: () -> Unit, + onClose: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Settings.OrderEgk.HealthCardOrderContactScreen), + topBarTitle = "", + listState = listState, + navigationMode = NavigationBarMode.Back, + onBack = onBack, + actions = { + TextButton(onClick = onClose) { + Text(stringResource(R.string.health_card_order_close)) + } + } + ) { + OrderHealthCardSelectMethodScreenContent( + listState, + selectedCompany, + selectedOption + ) + } +} + +@Composable +private fun OrderHealthCardSelectMethodScreenContent( + listState: LazyListState = rememberLazyListState(), + selectedCompany: HealthInsuranceCompany?, + selectedOption: OrderHealthCardContactOption +) { + selectedCompany?.let { + if (selectedCompany.noContactInformation()) { + NoContactInformation() + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Settings.OrderEgk.HealthCardOrderContactScreenContent) + .padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + state = listState, + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { + item { + val header = stringResource(R.string.order_health_card_contact_header) + val info = if (selectedCompany.singleContactInformation()) { + stringResource(R.string.order_health_card_contact_info_single) + } else { + stringResource(R.string.order_health_card_contact_info) + } + SpacerMedium() + Text( + text = header, + style = AppTheme.typography.h5, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + text = info, + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.neutral600, + textAlign = TextAlign.Center + ) + SpacerXXLarge() + } + + when (selectedOption) { + OrderHealthCardContactOption.WithHealthCardAndPin -> + item { + ContactMethodRow( + phone = it.healthCardAndPinPhone, + url = it.healthCardAndPinUrl, + mail = it.healthCardAndPinMail, + company = it, + option = selectedOption + ) + } + + else -> + item { + ContactMethodRow( + phone = it.healthCardAndPinPhone, + url = it.pinUrl, + mail = if (!it.subjectPinMail.isNullOrEmpty()) it.healthCardAndPinMail else null, + company = it, + option = selectedOption + ) + } + } + } + } + } +} + +@Composable +private fun NoContactInformation() { + Column( + modifier = Modifier.fillMaxSize().padding(PaddingDefaults.Medium), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.illustration_girl), + contentDescription = null, + alignment = Alignment.Center + ) + + Text( + stringResource(R.string.cdw_health_insurance_no_cantacts_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + + SpacerSmall() + Text( + stringResource(R.string.cdw_health_insurance_no_cantacts_body), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun ContactMethodRow( + phone: String?, + url: String?, + mail: String?, + company: HealthInsuranceCompany, + option: OrderHealthCardContactOption +) { + val uriHandler = LocalUriHandler.current + + val mailSubject = stringResource(R.string.cdw_health_insurance_mail_subject) + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(SizeDefaults.one) + ) { + phone?.let { + ContactMethod( + modifier = Modifier + .testTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton), + name = stringResource(R.string.healthcard_order_phone), + icon = Icons.Filled.PhoneInTalk, + onClick = { + uriHandler.openUriWhenValid("tel:$phone") + } + ) + } + url?.let { + ContactMethod( + modifier = Modifier + .testTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton), + name = stringResource(R.string.healthcard_order_website), + icon = Icons.Filled.OpenInBrowser, + onClick = { + uriHandler.openUriWhenValid(url) + } + ) + } + mail?.let { + val context = LocalContext.current + ContactMethod( + modifier = Modifier + .testTag(TestTag.Settings.ContactInsuranceCompany.MailToButton), + name = stringResource(R.string.healthcard_order_mail), + icon = Icons.Filled.MailOutline, + onClick = { + when { + option == OrderHealthCardContactOption.WithHealthCardAndPin && + company.hasMailContentForCardAndPin() -> openMailClient( + context = context, + address = mail, + subject = company.subjectCardAndPinMail!!, + body = company.bodyCardAndPinMail!! + ) + + option == OrderHealthCardContactOption.PinOnly && + company.hasMailContentForPin() -> openMailClient( + context = context, + address = mail, + subject = company.subjectPinMail!!, + body = company.bodyPinMail!! + ) + + else -> uriHandler.openUriWhenValid("mailto:$mail?subject=${Uri.encode(mailSubject)}") + } + } + ) + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun ContactMethod( + modifier: Modifier, + name: String, + icon: ImageVector, + onClick: () -> Unit +) { + Card( + modifier = modifier, + onClick = onClick, + shape = RoundedCornerShape(SizeDefaults.one), + contentColor = AppTheme.colors.primary600, + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.neutral300), + backgroundColor = AppTheme.colors.neutral050, + elevation = SizeDefaults.zero + ) { + Column(Modifier.padding(PaddingDefaults.Medium), horizontalAlignment = Alignment.CenterHorizontally) { + Icon(icon, null) + SpacerSmall() + Text( + name, + modifier = modifier.widthIn(SizeDefaults.ninefold), + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle2 + ) + } + } +} + +@LightDarkPreview +@Composable +fun OrderHealthCardSelectMethodScreenPreview() { + val listState = rememberLazyListState() + PreviewAppTheme { + OrderHealthCardSelectMethodScreenScaffold( + listState, + healthInsuranceCompany, + OrderHealthCardContactOption.NotChosen, + onBack = {}, + onClose = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectOptionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectOptionScreen.kt new file mode 100644 index 00000000..0d0bbb47 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/OrderHealthCardSelectOptionScreen.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardScreen +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardContactOption +import de.gematik.ti.erp.app.orderhealthcard.presentation.OrderHealthCardGraphController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class OrderHealthCardSelectOptionScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: OrderHealthCardGraphController +) : OrderHealthCardScreen() { + @Composable + override fun Content() { + val listState = rememberLazyListState() + OrderHealthCardSelectOptionScreenScaffold( + listState = listState, + onBack = { navController.popBackStack() }, + onClose = { navController.popBackStack(OrderHealthCardRoutes.subGraphName(), inclusive = true) }, + onSelectOption = { + graphController.setContactOption(it) + navController.navigate(OrderHealthCardRoutes.OrderHealthCardSelectMethodScreen.path()) + } + ) + } +} + +@Composable +private fun OrderHealthCardSelectOptionScreenScaffold( + listState: LazyListState, + onBack: () -> Unit, + onClose: () -> Unit, + onSelectOption: (OrderHealthCardContactOption) -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Settings.OrderEgk.SelectOrderOptionScreen), + topBarTitle = "", + listState = listState, + navigationMode = NavigationBarMode.Back, + onBack = onBack, + actions = { + TextButton(onClick = onClose) { + Text(stringResource(R.string.health_card_order_cancel)) + } + } + ) { + OrderHealthCardSelectOptionScreenContent( + listState = listState, + onSelectOption = onSelectOption + ) + } +} + +@Composable +private fun OrderHealthCardSelectOptionScreenContent( + listState: LazyListState, + onSelectOption: (OrderHealthCardContactOption) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Settings.OrderEgk.SelectOrderOptionContent) + .padding(horizontal = PaddingDefaults.Medium), + state = listState, + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { + item { + SpacerXXLarge() + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Image(painter = painterResource(R.drawable.egk_on_blue_circle), contentDescription = null) + } + } + item { + SpacerXXLarge() + SpacerXXLarge() + Text( + stringResource(id = R.string.select_order_option_header), + style = AppTheme.typography.h5, + textAlign = TextAlign.Center + ) + } + item { + SpacerSmall() + Text( + stringResource(id = R.string.select_order_option_info), + style = AppTheme.typography.subtitle2, + textAlign = TextAlign.Center + ) + SpacerXXLarge() + } + item { + Option( + testTag = TestTag.Settings.ContactInsuranceCompany.OrderPinButton, + name = stringResource(R.string.cdw_health_insurance_contact_pin_only), + onSelect = { onSelectOption(OrderHealthCardContactOption.PinOnly) } + ) + SpacerMedium() + } + item { + Option( + testTag = TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton, + name = stringResource(R.string.cdw_health_insurance_contact_healthcard_pin), + onSelect = { onSelectOption(OrderHealthCardContactOption.WithHealthCardAndPin) } + ) + } + } +} + +@Composable +private fun Option( + name: String, + testTag: String, + onSelect: () -> Unit +) { + val shape = RoundedCornerShape(SizeDefaults.double) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .testTag(testTag) + .clip(shape) + .border(1.dp, color = AppTheme.colors.neutral300, shape = shape) + .clickable(onClick = onSelect) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Text(name, style = AppTheme.typography.body1, modifier = Modifier.weight(1f)) + Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@LightDarkPreview +@Composable +fun OrderHealthCardSelectOptionScreenPreview() { + val listState = rememberLazyListState() + + PreviewAppTheme { + OrderHealthCardSelectOptionScreenScaffold( + listState, + onBack = {}, + onClose = {}, + onSelectOption = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/preview/OrderHealthCardPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/preview/OrderHealthCardPreviewParameter.kt new file mode 100644 index 00000000..fb5589b3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/preview/OrderHealthCardPreviewParameter.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.orderhealthcard.presentation.HealthInsuranceCompany +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.OrderHealthCardPreviewData.healthInsuranceCompanyData +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.OrderHealthCardPreviewData.healthInsuranceCompanyDataWithSearchText + +data class HealthInsuranceSearchData( + val healthInsurance: HealthInsuranceCompany, + val searchText: String = "" +) +class OrderHealthCardPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + healthInsuranceCompanyData, + healthInsuranceCompanyDataWithSearchText + ) +} + +object OrderHealthCardPreviewData { + + val healthInsuranceCompany = HealthInsuranceCompany( + name = "Insurance Company", + healthCardAndPinPhone = "+123123", + healthCardAndPinMail = "", + healthCardAndPinUrl = "https://www.TestURL.de/", + pinUrl = "https://www.TestPinURL.de/", + subjectCardAndPinMail = "testHeader", + bodyCardAndPinMail = "testBody", + subjectPinMail = "testHeader", + bodyPinMail = "testBody" + ) + + val healthInsuranceCompanyData = HealthInsuranceSearchData( + healthInsuranceCompany + + ) + + val healthInsuranceCompanyDataWithSearchText = HealthInsuranceSearchData( + healthInsuranceCompany, + "Insurance Company G" + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt deleted file mode 100644 index f96eca84..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard.usecase - -import android.content.Context -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.serialization.json.Json -import java.io.InputStream - -class HealthCardOrderUseCase( - private val context: Context -) { - private val companies: List by lazy { - loadHealthInsuranceContactsFromJSON( - context.resources.openRawResourceFd(R.raw.health_insurance_contacts).createInputStream() - ).sortedBy { it.name.lowercase() } - } - - val healthInsuranceOrderContacts: Flow> - get() = flow { - emit(companies) - } -} - -fun loadHealthInsuranceContactsFromJSON( - jsonInput: InputStream -): List = - Json.decodeFromString(jsonInput.bufferedReader().readText()) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCase.kt new file mode 100644 index 00000000..5651ee23 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/LoadHealthInsuranceListUseCase.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.usecase + +import android.content.Context +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.orderhealthcard.presentation.HealthInsuranceCompany +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.serialization.json.Json +import java.io.InputStream + +class LoadHealthInsuranceListUseCase( + private val context: Context +) { + private val companies: List by lazy { + loadHealthInsuranceContactsFromJSON( + context.resources.openRawResourceFd(R.raw.health_insurance_contacts).createInputStream() + ).sortedBy { it.name.lowercase() } + } + + operator fun invoke(): Flow> = flow { + emit(companies) + } +} + +fun loadHealthInsuranceContactsFromJSON( + jsonInput: InputStream +): List = + Json.decodeFromString(jsonInput.bufferedReader().readText()) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt deleted file mode 100644 index b189ed18..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orderhealthcard.usecase.model - -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -object HealthCardOrderUseCaseData { - @Immutable - @Serializable - data class HealthInsuranceCompany( - val name: String, - val healthCardAndPinPhone: String?, - val healthCardAndPinMail: String?, - val healthCardAndPinUrl: String?, - val pinUrl: String?, - val subjectCardAndPinMail: String?, - val bodyCardAndPinMail: String?, - val subjectPinMail: String?, - val bodyPinMail: String? - ) { - fun noContactInformation() = - healthCardAndPinPhone.isNullOrEmpty() && - healthCardAndPinMail.isNullOrEmpty() && - healthCardAndPinUrl.isNullOrEmpty() && - pinUrl.isNullOrEmpty() - - fun singleContactInformation() = - ( - !healthCardAndPinPhone.isNullOrEmpty() && - healthCardAndPinMail.isNullOrEmpty() && - healthCardAndPinUrl.isNullOrEmpty() && - pinUrl.isNullOrEmpty() - ) || - ( - healthCardAndPinPhone.isNullOrEmpty() && - !healthCardAndPinMail.isNullOrEmpty() && - healthCardAndPinUrl.isNullOrEmpty() && - pinUrl.isNullOrEmpty() - ) || - ( - healthCardAndPinPhone.isNullOrEmpty() && - healthCardAndPinMail.isNullOrEmpty() && - !healthCardAndPinUrl.isNullOrEmpty() && - !pinUrl.isNullOrEmpty() - ) - - fun hasContactInfoForPin() = - !pinUrl.isNullOrEmpty() || ( - !healthCardAndPinMail.isNullOrEmpty() && - !bodyPinMail.isNullOrEmpty() && !subjectPinMail.isNullOrEmpty() - ) - - fun hasContactInfoForHealthCardAndPin() = - !healthCardAndPinPhone.isNullOrEmpty() || - !healthCardAndPinMail.isNullOrEmpty() || - !healthCardAndPinUrl.isNullOrEmpty() - - fun hasMailContentForCardAndPin() = !subjectCardAndPinMail.isNullOrEmpty() && - !bodyCardAndPinMail.isNullOrEmpty() - - fun hasMailContentForPin() = !subjectPinMail.isNullOrEmpty() && !bodyPinMail.isNullOrEmpty() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt deleted file mode 100644 index fe2932d5..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders - -import de.gematik.ti.erp.app.orders.repository.CommunicationLocalDataSource -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.repository.DefaultCommunicationRepository -import de.gematik.ti.erp.app.orders.repository.PharmacyCacheLocalDataSource -import de.gematik.ti.erp.app.orders.repository.PharmacyCacheRemoteDataSource -import de.gematik.ti.erp.app.orders.usecase.GetRepliedMessagesUseCase -import de.gematik.ti.erp.app.orders.usecase.GetOrderUsingOrderIdUseCase -import de.gematik.ti.erp.app.orders.usecase.GetOrdersUsingProfileIdUseCase -import de.gematik.ti.erp.app.orders.usecase.GetUnreadOrdersUseCase -import de.gematik.ti.erp.app.orders.usecase.SaveLocalCommunicationUseCase -import de.gematik.ti.erp.app.orders.usecase.UpdateCommunicationByCommunicationIdUseCase -import de.gematik.ti.erp.app.orders.usecase.UpdateCommunicationByOrderIdUseCase -import org.kodein.di.DI -import org.kodein.di.bindProvider -import org.kodein.di.instance - -val messagesModule = DI.Module("messagesModule") { - bindProvider { PharmacyCacheLocalDataSource(instance()) } - bindProvider { PharmacyCacheRemoteDataSource(instance()) } - bindProvider { CommunicationLocalDataSource(instance()) } - bindProvider { GetRepliedMessagesUseCase(instance(), instance()) } - bindProvider { GetOrdersUsingProfileIdUseCase(instance()) } - bindProvider { GetOrderUsingOrderIdUseCase(instance(), instance()) } - bindProvider { GetUnreadOrdersUseCase(instance()) } - bindProvider { SaveLocalCommunicationUseCase(instance()) } - bindProvider { UpdateCommunicationByOrderIdUseCase(instance()) } - bindProvider { UpdateCommunicationByCommunicationIdUseCase(instance()) } -} - -val messageRepositoryModule = DI.Module("messageRepositoryModule", allowSilentOverride = true) { - bindProvider { - DefaultCommunicationRepository( - instance(), - instance(), - instance(), - instance(), - instance(), - instance() - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/mappers/CommunicationMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/mappers/CommunicationMapper.kt deleted file mode 100644 index bd2854fa..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/mappers/CommunicationMapper.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.mappers - -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.pharmacy.repository.model.CommunicationPayloadInbox -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import java.net.URI - -private val lenientJson = Json { - isLenient = true - ignoreUnknownKeys = true -} - -fun Communication.toOrder( - prescriptions: List, - hasUnreadMessages: Boolean, - taskIds: List, - pharmacyName: String? -) = - OrderUseCaseData.Order( - orderId = orderId, - taskIds = taskIds, - prescriptions = prescriptions, - sentOn = sentOn, - pharmacy = OrderUseCaseData.Pharmacy(name = pharmacyName ?: "", id = this.recipient), - hasUnreadMessages = hasUnreadMessages - ) - -fun Communication.toOrderDetail( - hasUnreadMessages: Boolean, - taskDetailedBundles: List, - pharmacyName: String? -) = - OrderUseCaseData.OrderDetail( - orderId = orderId, - taskDetailedBundles = taskDetailedBundles, - sentOn = sentOn, - pharmacy = OrderUseCaseData.Pharmacy(name = pharmacyName ?: "", id = this.recipient), - hasUnreadMessages = hasUnreadMessages - ) - -fun Communication.toMessage(hasInvoice: Boolean = false) = - payload?.let { - try { - val inbox = lenientJson.decodeFromString(it) - - OrderUseCaseData.Message( - communicationId = communicationId, - sentOn = sentOn, - message = inbox.infoText?.ifBlank { null }, - code = inbox.pickUpCodeDMC?.ifBlank { null } ?: inbox.pickUpCodeHR?.ifBlank { null }, - link = inbox.url?.ifBlank { null }?.takeIf { isValidUrl(it) }, - consumed = consumed, - hasInvoice = hasInvoice - ) - } catch (ignored: SerializationException) { - OrderUseCaseData.Message( - communicationId = communicationId, - sentOn = sentOn, - message = null, - code = null, - link = null, - consumed = consumed, - hasInvoice = hasInvoice - ) - } - } ?: OrderUseCaseData.Message( - communicationId = communicationId, - sentOn = sentOn, - message = null, - code = null, - link = null, - consumed = consumed, - hasInvoice = hasInvoice - ) - -/** - * Every url should be valid and the scheme is `https`. - */ -fun isValidUrl(url: String): Boolean = - try { - URI.create(url).scheme == "https" - } catch (_: IllegalArgumentException) { - false - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/MessageController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/MessageController.kt deleted file mode 100644 index c6fc5931..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/MessageController.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.orders.usecase.GetOrderUsingOrderIdUseCase -import de.gematik.ti.erp.app.orders.usecase.GetRepliedMessagesUseCase -import de.gematik.ti.erp.app.orders.usecase.UpdateCommunicationByCommunicationIdUseCase -import de.gematik.ti.erp.app.orders.usecase.UpdateCommunicationByOrderIdUseCase -import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.withContext -import org.kodein.di.compose.rememberInstance - -@Stable -class MessageController( - orderId: String, - private val getActiveProfileUseCase: GetActiveProfileUseCase, - private val getRepliedMessagesUseCase: GetRepliedMessagesUseCase, - private val getOrderUsingOrderIdUseCase: GetOrderUsingOrderIdUseCase, - private val updateCommunicationByCommunicationIdUseCase: UpdateCommunicationByCommunicationIdUseCase, - private val updateCommunicationByOrderIdUseCase: UpdateCommunicationByOrderIdUseCase, - coroutineScope: CoroutineScope -) { - enum class States { - LoadingMessages, - HasMessages, - NoMessages - } - - var state by mutableStateOf(States.LoadingMessages) - private set - - val activeProfile by lazy { - getActiveProfileUseCase() - } - - private val messageFlow = getRepliedMessagesUseCase - .invoke(orderId) - .onEach { - state = if (it.isEmpty()) { - States.NoMessages - } else { - States.HasMessages - } - Napier.d("state is $state") - } - .shareIn(coroutineScope, SharingStarted.Lazily, 1) - - val messages - @Composable - get() = messageFlow - .collectAsStateWithLifecycle(emptyList()) - - private val orderFlow = getOrderUsingOrderIdUseCase - .invoke(orderId) - .shareIn(coroutineScope, SharingStarted.Lazily, 1) - - val order - @Composable - get() = orderFlow - .collectAsStateWithLifecycle(null) - - val activeProfileState - @Composable - get() = activeProfile.collectAsStateWithLifecycle(null) - - suspend fun consumeAllMessages() { - withContext(NonCancellable) { - orderFlow.first()?.let { - if (it.hasUnreadMessages) { - updateCommunicationByOrderIdUseCase.invoke(it.orderId) - messageFlow.first().forEach { - updateCommunicationByCommunicationIdUseCase.invoke(it.communicationId) - } - } - } - } - } -} - -@Composable -fun rememberMessageController( - orderId: String -): MessageController { - val coroutineScope = rememberCoroutineScope() - val getActiveProfileUseCase: GetActiveProfileUseCase by rememberInstance() - val getRepliedMessagesUseCase: GetRepliedMessagesUseCase by rememberInstance() - val getOrderUsingOrderIdUseCase: GetOrderUsingOrderIdUseCase by rememberInstance() - val updateCommunicationByCommunicationIdUseCase: UpdateCommunicationByCommunicationIdUseCase by rememberInstance() - val updateCommunicationByOrderIdUseCase: UpdateCommunicationByOrderIdUseCase by rememberInstance() - - return remember(orderId) { - MessageController( - orderId = orderId, - getActiveProfileUseCase = getActiveProfileUseCase, - getRepliedMessagesUseCase = getRepliedMessagesUseCase, - getOrderUsingOrderIdUseCase = getOrderUsingOrderIdUseCase, - updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, - updateCommunicationByOrderIdUseCase = updateCommunicationByOrderIdUseCase, - coroutineScope = coroutineScope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/OrderController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/OrderController.kt deleted file mode 100644 index ce837c37..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/presentation/OrderController.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.orders.usecase.GetOrdersUsingProfileIdUseCase -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.flow.onEach -import org.kodein.di.compose.rememberInstance - -@Stable -class OrderController( - profileIdentifier: ProfileIdentifier, - getOrdersUsingProfileIdUseCase: GetOrdersUsingProfileIdUseCase -) { - enum class States { - LoadingOrders, - HasOrders, - NoOrders - } - - var state by mutableStateOf(States.LoadingOrders) - private set - - private val orderFlow = getOrdersUsingProfileIdUseCase - .invoke(profileIdentifier) - .onEach { - state = if (it.isEmpty()) { - States.NoOrders - } else { - States.HasOrders - } - } - - val orders - @Composable - get() = orderFlow.collectAsStateWithLifecycle(emptyList()) -} - -@Composable -fun rememberOrderState( - profileIdentifier: ProfileIdentifier -): OrderController { - val getOrdersUsingProfileIdUseCase by rememberInstance() - return remember(profileIdentifier) { - OrderController(profileIdentifier, getOrdersUsingProfileIdUseCase) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DispenseMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DispenseMessage.kt deleted file mode 100644 index abbfca92..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DispenseMessage.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -internal fun DispenseMessage( - hasReplyMessages: Boolean, - pharmacyName: String, - orderSentOn: Instant -) { - val date = remember(orderSentOn) { - val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - dateFormatter.format(orderSentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) - } - val time = remember(orderSentOn) { - val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - dateFormatter.format(orderSentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) - } - - Row( - Modifier.drawConnectedLine( - drawTop = true, - drawBottom = false, - topDashed = !hasReplyMessages - ) - ) { - Spacer(Modifier.width(48.dp)) - Column( - Modifier - .weight(1f) - .padding(PaddingDefaults.Medium) - ) { - SpacerTiny() - Text( - stringResource(R.string.orders_timestamp, date, time), - style = AppTheme.typography.subtitle2 - ) - - val highlightedPharmacyName = buildAnnotatedString { - withStyle(SpanStyle(color = AppTheme.colors.primary600)) { - append(pharmacyName) - } - } - Text( - text = annotatedStringResource( - R.string.orders_prescription_sent_to, - highlightedPharmacyName - ), - style = AppTheme.typography.body2l - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DrawConnectedLine.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DrawConnectedLine.kt deleted file mode 100644 index 7caccbfc..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/DrawConnectedLine.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import android.content.res.Resources -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.AppTheme - -internal fun Modifier.drawConnectedLine( - drawTop: Boolean, - drawBottom: Boolean, - topDashed: Boolean = false -) = composed { - val lineColor = AppTheme.colors.neutral300 - val circleBackground = AppTheme.colors.neutral000 - val strokeWidth = 2.dp.toPx() - val circleRadius = 8.dp.toPx() - val backgroundRadius = 3.dp.toPx() - - drawBehind { - val center = Offset(x = 24.dp.toPx(), y = size.height / 2) - val start = if (drawTop) Offset(x = center.x, y = 0f) else center - val end = if (drawBottom) Offset(x = center.x, y = size.height) else center - - drawLine( - color = lineColor, - strokeWidth = strokeWidth, - start = start, - end = end, - pathEffect = if (topDashed) { - PathEffect.dashPathEffect( - floatArrayOf( - 5.dp.toPx(), - 2.dp.toPx() - ) - ) - } else { - null - } - ) - - drawCircle(color = lineColor, center = center, radius = circleRadius) - drawCircle(color = circleBackground, center = center, radius = backgroundRadius) - } -} - -private fun Dp.toPx(): Float { - val density = Resources.getSystem().displayMetrics.density - return this.value * density -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/InvoiceMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/InvoiceMessage.kt deleted file mode 100644 index ce6a1f2c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/InvoiceMessage.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -internal fun InvoiceMessage( - taskId: String, - prescriptionName: String?, - prescriptionDate: Instant?, - onClickCostReceiptDetail: () -> Unit -) { - Row( - Modifier.drawConnectedLine( - drawTop = true, - drawBottom = true, - topDashed = false - ) - ) { - Spacer(Modifier.width(48.dp)) - Column( - Modifier - .weight(1f) - .padding(PaddingDefaults.Medium) - ) { - SpacerTiny() - val date = remember(taskId) { - val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - dateFormatter.format( - prescriptionDate - ?.toLocalDateTime(TimeZone.currentSystemDefault()) - ?.toJavaLocalDateTime() - ) - } - val time = remember(taskId) { - val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - dateFormatter.format( - prescriptionDate - ?.toLocalDateTime(TimeZone.currentSystemDefault())?.toJavaLocalDateTime() - ) - } - Spacer(Modifier.height(18.dp)) - Text( - stringResource(R.string.orders_timestamp, date, time), - style = AppTheme.typography.subtitle2 - ) - prescriptionName?.let { - Text( - text = annotatedStringResource( - R.string.cost_receipt_is_ready, - it - ), - style = AppTheme.typography.body2l - ) - } - SpacerSmall() - CostReceiptDetail( - onClick = { onClickCostReceiptDetail() } - ) - } - } -} - -@Composable -fun CostReceiptDetail( - onClick: () -> Unit -) { - Row(modifier = Modifier.clickable(onClick = onClick)) { - Text( - text = stringResource(R.string.show_cost_receipt), - style = AppTheme.typography.body2l, - color = AppTheme.colors.primary600 - ) - Icon( - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterVertically), - imageVector = Icons.Outlined.KeyboardArrowRight, - contentDescription = null, - tint = AppTheme.colors.primary600 - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt deleted file mode 100644 index 3f5f9caf..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.createBitMatrix -import de.gematik.ti.erp.app.utils.compose.drawDataMatrix -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -fun MessageSheetContent( - order: OrderUseCaseData.OrderDetail?, - message: OrderUseCaseData.Message?, - onClickClose: () -> Unit -) { - Column( - Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium) - .padding(bottom = PaddingDefaults.XLarge) - .navigationBarsPadding() - ) { - IconButton( - onClick = onClickClose, - modifier = Modifier.align(Alignment.End) - ) { - Box( - Modifier - .size(32.dp) - .background(AppTheme.colors.neutral100, CircleShape), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Rounded.Close, null, tint = AppTheme.colors.neutral600) - } - } - SpacerMedium() - order?.let { - message?.let { - Box( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .testTag(TestTag.Orders.Messages.Content) - ) { - when (message.type) { - OrderUseCaseData.Message.Type.All -> - AllSheetContent(message) - - OrderUseCaseData.Message.Type.Link -> - LinkSheetContent(message) - - OrderUseCaseData.Message.Type.Code -> - CodeSheetContent(message) - - OrderUseCaseData.Message.Type.Text -> - TextSheetContent(message) - - OrderUseCaseData.Message.Type.Empty -> - EmptySheetContent(order.pharmacy.pharmacyName()) - } - } - } - } - } -} - -@Composable -private fun AllSheetContent( - message: OrderUseCaseData.Message -) { - Column(verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large)) { - message.code?.let { - Box( - Modifier - .background(AppTheme.colors.neutral100, RoundedCornerShape(16.dp)) - .padding(PaddingDefaults.Medium) - ) { - DataMatrixCode(payload = message.code, modifier = Modifier.size(144.dp)) - } - CodeLabel(code = message.code) - } - message.message?.let { - TextSheetContent(message) - } - message.link?.let { - LinkSheetContent(message) - } - } -} - -@Composable -fun LinkSheetContent( - message: OrderUseCaseData.Message -) { - val uriHandler = LocalUriHandler.current - - Column( - modifier = Modifier - .fillMaxWidth() - .testTag(TestTag.Orders.Messages.Link), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (message.type != OrderUseCaseData.Message.Type.All) { - Text( - stringResource(R.string.orders_cart_ready), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.orders_cart_ready_info), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - SpacerLarge() - } - PrimaryButtonSmall( - modifier = Modifier.testTag(TestTag.Orders.Messages.LinkButton), - onClick = { - message.link?.let { uriHandler.openUri(it) } - } - ) { - Text(stringResource(R.string.orders_open_cart_link)) - } - } -} - -@Composable -fun TextSheetContent( - message: OrderUseCaseData.Message -) { - Column( - modifier = Modifier - .fillMaxWidth() - .semantics(true) { testTag = TestTag.Orders.Messages.Text } - ) { - Box( - Modifier - .fillMaxWidth() - .background(AppTheme.colors.neutral100, RoundedCornerShape(16.dp)) - .padding(PaddingDefaults.Medium) - ) { - Text( - message.message ?: "", - style = AppTheme.typography.body2 - ) - } - SpacerSmall() - Text( - sentOn(message), - style = AppTheme.typography.caption1l, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.End) - ) - } -} - -@Composable -private fun sentOn(message: OrderUseCaseData.Message): String = - remember(message) { - val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - dateFormatter.format(message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) - } - -@Composable -fun CodeSheetContent( - message: OrderUseCaseData.Message -) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - message.code?.let { - DataMatrixCode(payload = message.code, modifier = Modifier.size(144.dp)) - SpacerMedium() - CodeLabel(code = message.code) - } - SpacerSmall() - Text( - stringResource(R.string.orders_code_title), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.orders_code_info), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun CodeLabel( - code: String -) { - Box( - Modifier - .background(AppTheme.colors.neutral100, RoundedCornerShape(8.dp)) - .padding(horizontal = PaddingDefaults.ShortMedium, vertical = PaddingDefaults.ShortMedium / 2) - .semantics(true) { testTag = TestTag.Orders.Messages.CodeLabelContent } - ) { - Text( - code, - style = AppTheme.typography.subtitle2l, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } -} - -@Composable -fun EmptySheetContent(pharmacyName: String) { - Column( - Modifier - .fillMaxWidth() - .testTag(TestTag.Orders.Messages.Empty), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(R.string.orders_no_message_title), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.orders_no_message, pharmacyName), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun DataMatrixCode(payload: String, modifier: Modifier) { - val matrix = remember(payload) { createBitMatrix(payload) } - - Box( - modifier = Modifier - .then(modifier) - .background(Color.White) - .padding(PaddingDefaults.Small) - .testTag(TestTag.Orders.Messages.Code) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .drawDataMatrix(matrix) - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Messages.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Messages.kt deleted file mode 100644 index ac7a63e2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Messages.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orders.presentation.MessageController -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription -import de.gematik.ti.erp.app.prescriptionId -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import io.github.aakira.napier.Napier - -@Suppress("LongMethod") -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun Messages( - listState: LazyListState, - messageController: MessageController, - onClickMessage: (OrderUseCaseData.Message) -> Unit, - onClickPrescription: (String) -> Unit, - onClickInvoiceMessage: (String) -> Unit -) { - val order by messageController.order - val messages by messageController.messages - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.Orders.Details.Content), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) - .asPaddingValues() - ) { - item { - SpacerMedium() - Text( - stringResource(R.string.orders_history_title), - style = AppTheme.typography.h6, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - SpacerMedium() - } - - when (messageController.state) { - MessageController.States.HasMessages -> { - Napier.d { "checking has message: " } - messages.forEachIndexed { index, message -> - item { - Napier.d { "checking invoices! = ${message.hasInvoice}" } - ReplyMessage( - message = message, - isFirstMessage = index == 0, - onClick = { - onClickMessage(message) - } - ) - } - } - } - - else -> {} - } - - order?.let { orderDetail: OrderUseCaseData.OrderDetail -> - itemsIndexed( - items = orderDetail.taskDetailedBundles - ) { _, taskBundle -> - if (taskBundle.hasInvoice) { - InvoiceMessage( - taskId = taskBundle.taskId, - prescriptionName = taskBundle.prescription?.name ?: "", - prescriptionDate = taskBundle.prescription?.startedOn, - onClickCostReceiptDetail = { - onClickInvoiceMessage(taskBundle.taskId) - } - ) - } - } - item { - DispenseMessage( - hasReplyMessages = messages.isNotEmpty(), - pharmacyName = orderDetail.pharmacy.name, - orderSentOn = orderDetail.sentOn - ) - SpacerXXLarge() - } - } - - item { - Divider(color = AppTheme.colors.neutral300) - SpacerXXLarge() - Text( - stringResource(R.string.orders_cart_title), - style = AppTheme.typography.h6, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - } - - order?.let { - item(key = "prescriptions") { - Column( - Modifier.padding(PaddingDefaults.Medium), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - it.taskDetailedBundles.forEachIndexed { index, taskBundle -> - Surface( - modifier = Modifier - .testTag(TestTag.Orders.Details.PrescriptionListItem) - .semantics { - prescriptionId = it.taskDetailedBundles[index].taskId - }, - shape = RoundedCornerShape(8.dp), - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - color = AppTheme.colors.neutral050, - onClick = { - onClickPrescription(it.taskDetailedBundles[index].taskId) - } - ) { - Row( - Modifier.padding(PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - val titlePrepend = - stringResource(R.string.pres_details_scanned_medication) - - val name = when (taskBundle.prescription) { - is Prescription.ScannedPrescription -> - taskBundle.prescription.name - ?: "$titlePrepend ${taskBundle.prescription.index}" - - is Prescription.SyncedPrescription -> taskBundle.prescription.name ?: "" - else -> "" - } - Text( - name, - style = AppTheme.typography.subtitle1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) - ) - SpacerMedium() - Icon( - Icons.Rounded.KeyboardArrowRight, - contentDescription = null, - tint = AppTheme.colors.neutral400 - ) - } - } - } - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt deleted file mode 100644 index 9c51daab..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.ui.EmptyScreenHome -import de.gematik.ti.erp.app.prescription.ui.HomeConnectedWithoutToken -import de.gematik.ti.erp.app.prescription.ui.HomeConnectedWithoutTokenBiometrics -import de.gematik.ti.erp.app.prescription.ui.HomeHealthCardDisconnected -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.ProfileConnectionState -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -@Composable -fun LazyItemScope.OrderEmptyScreen( - connectionState: ProfileConnectionState?, - onClickRefresh: () -> Unit -) { - Box( - modifier = Modifier - .fillParentMaxSize() - .padding(PaddingDefaults.Medium), - contentAlignment = Alignment.Center - ) { - when (connectionState) { - ProfileConnectionState.LoggedOut -> { - HomeHealthCardDisconnected( - onClickAction = onClickRefresh - ) - } - ProfileConnectionState.LoggedOutWithoutTokenBiometrics -> { - HomeConnectedWithoutTokenBiometrics( - onClickAction = onClickRefresh - ) - } - ProfileConnectionState.LoggedOutWithoutToken -> { - HomeConnectedWithoutToken( - onClickAction = onClickRefresh - ) - } - else -> { - NoOrders( - onClickRefresh = onClickRefresh - ) - } - } - } -} - -@Composable -private fun NoOrders( - modifier: Modifier = Modifier, - onClickRefresh: () -> Unit -) = - EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.orders_empty_title), - description = stringResource(R.string.orders_empty_subtitle), - image = { - Image( - painterResource(R.drawable.woman_red_shirt_circle_blue), - contentDescription = null, - modifier = Modifier.size(160.dp) - ) - }, - button = { - TextButton( - onClick = onClickRefresh - ) { - Icon( - Icons.Rounded.Refresh, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = stringResource(R.string.home_egk_redeemed_buttontext), textAlign = TextAlign.Right) - } - } - ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderOverviewScreen.kt deleted file mode 100644 index 66ef2088..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderOverviewScreen.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import android.net.Uri -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackOrderPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orders.presentation.rememberMessageController -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun OrderOverviewScreen( - orderId: String, - mainNavController: NavController -) { - val listState = rememberLazyListState() - - val messageController = rememberMessageController(orderId = orderId) - - val order by messageController.order - - val activeProfile by messageController.activeProfileState - - val sheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden, - confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded } - ) - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - analytics.trackOrderPopUps() - } else { - analytics.onPopUpClosed() - val route = Uri.parse(mainNavController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - - val scope = rememberCoroutineScope() - var selectedMessage: OrderUseCaseData.Message? by remember { mutableStateOf(null) } -// Todo: It should not be inside bottomsheet - ModalBottomSheetLayout( - sheetState = sheetState, - sheetContent = { - MessageSheetContent( - order = order, - message = selectedMessage, - onClickClose = { scope.launch { sheetState.hide() } } - ) - }, - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) } - ) { - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Orders.Details.Screen), - topBarTitle = stringResource(R.string.orders_details_title), - listState = listState, - navigationMode = NavigationBarMode.Back, - onBack = { - scope.launch(Dispatchers.Main) { - messageController.consumeAllMessages() - mainNavController.popBackStack() - } - } - ) { - Messages( - listState = listState, - messageController = messageController, - onClickMessage = { - selectedMessage = it - scope.launch { sheetState.show() } - }, - onClickPrescription = { - mainNavController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailScreen.path(taskId = it) - ) - }, - onClickInvoiceMessage = { taskId -> - activeProfile?.let { profile -> - mainNavController.navigate( - PkvRoutes.InvoiceDetailsScreen - .path( - taskId = taskId, - profileId = profile.id - ) - ) - } - } - ) - } - } - - BackHandler { - scope.launch { - messageController.consumeAllMessages() - mainNavController.popBackStack() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt deleted file mode 100644 index 21da6f33..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.ui.UserNotAuthenticatedDialog -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource - -@Composable -fun OrderScreen( - mainNavController: NavController, - mainScreenController: MainScreenController, - onElevateTopBar: (Boolean) -> Unit -) { - val profileController = rememberProfileController() - val activeProfile by profileController.getActiveProfileState() - - var showUserNotAuthenticatedDialog by remember { mutableStateOf(false) } - - val onShowCardWall = { - mainNavController.navigate( - CardWallRoutes.CardWallIntroScreen.path(activeProfile.id) - ) - } - if (showUserNotAuthenticatedDialog) { - UserNotAuthenticatedDialog( - onCancel = { showUserNotAuthenticatedDialog = false }, - onShowCardWall = onShowCardWall - ) - } - - RefreshScaffold( - profileId = activeProfile.id, - onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, - mainScreenController = mainScreenController, - onShowCardWall = onShowCardWall - ) { onRefresh -> - Orders( - activeProfile = activeProfile, - onClickOrder = { orderId -> - mainNavController.navigate( - MainNavigationScreens.Messages.path(orderId) - ) - }, - onClickRefresh = { - onRefresh(true, MutatePriority.UserInput) - }, - onElevateTopBar = onElevateTopBar - ) - } -} - -@Composable -fun NewLabel() { - Box( - Modifier - .clip(CircleShape) - .background(AppTheme.colors.primary100) - .padding(horizontal = PaddingDefaults.Small, vertical = 3.dp), - contentAlignment = Alignment.Center - ) { - Text( - stringResource(R.string.orders_label_new), - style = AppTheme.typography.caption2, - color = AppTheme.colors.primary900 - ) - } -} - -@Composable -fun PrescriptionLabel(count: Int) { - Box( - Modifier - .clip(CircleShape) - .background(AppTheme.colors.neutral100) - .padding(horizontal = PaddingDefaults.Small, vertical = 3.dp), - contentAlignment = Alignment.Center - ) { - Text( - annotatedPluralsResource( - R.plurals.orders_plurals_label_nr_of_prescriptions, - count, - AnnotatedString(count.toString()) - ), - style = AppTheme.typography.caption2, - color = AppTheme.colors.neutral600 - ) - } -} - -@Composable -fun OrderUseCaseData.Pharmacy.pharmacyName() = - name.ifBlank { - stringResource(R.string.orders_generic_pharmacy_name) - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Orders.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Orders.kt deleted file mode 100644 index e9520c5b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/Orders.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.orders.presentation.OrderController -import de.gematik.ti.erp.app.orders.presentation.rememberOrderState -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.connectionState -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.timeDescription - -@Composable -internal fun Orders( - activeProfile: ProfilesUseCaseData.Profile, - onClickOrder: (orderId: String) -> Unit, - onClickRefresh: () -> Unit, - onElevateTopBar: (Boolean) -> Unit -) { - val listState = rememberLazyListState() - val orderState = rememberOrderState(activeProfile.id) - val orders by orderState.orders - - LaunchedEffect(Unit) { - snapshotFlow { - listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 - }.collect { - onElevateTopBar(it) - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.Orders.Content), - state = listState - ) { - when (orderState.state) { - OrderController.States.LoadingOrders -> { - // keep empty - item {} - } - - OrderController.States.HasOrders -> { - orders.forEachIndexed { index, order -> - item { - val sentOn by timeDescription(order.sentOn) - Order( - pharmacy = order.pharmacy.pharmacyName(), - time = sentOn, - hasUnreadMessages = order.hasUnreadMessages, - nrOfPrescriptions = order.taskIds.size, - onClick = { - onClickOrder(order.orderId) - } - ) - if (index < orders.size - 1) { - Divider(Modifier.padding(start = PaddingDefaults.Medium)) - } - } - } - } - - OrderController.States.NoOrders -> { - item { - val connectionState = activeProfile.connectionState() - OrderEmptyScreen(connectionState, onClickRefresh = onClickRefresh) - } - } - } - } -} - -@Composable -private fun Order( - pharmacy: String, - time: String, - hasUnreadMessages: Boolean, - nrOfPrescriptions: Int, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clickable(onClick = onClick) - .padding(PaddingDefaults.Medium) - .fillMaxWidth() - .testTag(TestTag.Orders.OrderListItem), - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), - verticalAlignment = Alignment.CenterVertically - ) { - Column(Modifier.weight(1f)) { - Text(pharmacy, style = AppTheme.typography.subtitle1) - SpacerTiny() - Text(time, style = AppTheme.typography.body2l) - } - if (hasUnreadMessages) { - NewLabel() - } else { - PrescriptionLabel(nrOfPrescriptions) - } - Icon(Icons.Rounded.KeyboardArrowRight, contentDescription = null, tint = AppTheme.colors.neutral400) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/ReplyMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/ReplyMessage.kt deleted file mode 100644 index 1699a50b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/ReplyMessage.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.DynamicText -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -internal fun ReplyMessage( - message: OrderUseCaseData.Message, - isFirstMessage: Boolean, - onClick: () -> Unit -) { - val info = when (message.type) { - OrderUseCaseData.Message.Type.Link -> stringResource(R.string.orders_show_cart) - OrderUseCaseData.Message.Type.Code -> stringResource(R.string.orders_show_code) - OrderUseCaseData.Message.Type.Text -> null - else -> stringResource(R.string.orders_show_general_message) - } - val description = when (message.type) { - OrderUseCaseData.Message.Type.Text -> message.message ?: "" - else -> null - } - - val date = remember(message) { - val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - dateFormatter.format( - message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() - ) - } - val time = remember(message) { - val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - dateFormatter.format( - message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() - ) - } - - Column( - Modifier - .drawConnectedLine( - drawTop = !isFirstMessage, - drawBottom = true, - topDashed = false - ) - .clickable( - onClick = onClick, - enabled = message.type != OrderUseCaseData.Message.Type.Text - ) - .fillMaxWidth() - .testTag(TestTag.Orders.Details.MessageListItem) - ) { - Row { - Spacer(Modifier.width(48.dp)) - Column( - Modifier - .weight(1f) - .padding(PaddingDefaults.Medium) - ) { - Text( - stringResource(R.string.orders_timestamp, date, time), - style = AppTheme.typography.subtitle2 - ) - description?.let { - SpacerTiny() - Text( - text = it, - style = AppTheme.typography.body2l - ) - } - info?.let { - SpacerTiny() - val txt = buildAnnotatedString { - append(it) - append(" ") - appendInlineContent("button", "button") - } - val c = mapOf( - "button" to InlineTextContent( - Placeholder( - width = 0.em, - height = 0.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter - ) - ) { - Icon( - Icons.Rounded.KeyboardArrowRight, - contentDescription = null, - tint = AppTheme.colors.primary600 - ) - } - ) - DynamicText( - txt, - style = AppTheme.typography.body2, - color = AppTheme.colors.primary600, - inlineContent = c - ) - } - } - } - Divider(Modifier.padding(start = 64.dp)) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/DispenseRequestCommunicationToOrder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/DispenseRequestCommunicationToOrder.kt deleted file mode 100644 index ee209d36..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/DispenseRequestCommunicationToOrder.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.mappers.toOrder -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.mapper.toPrescription -import de.gematik.ti.erp.app.prescription.model.Communication -import kotlinx.coroutines.flow.first - -suspend fun Communication.dispenseRequestCommunicationToOrder( - communicationRepository: CommunicationRepository, - withMedicationNames: Boolean, - pharmacyName: String? -): OrderUseCaseData.Order { - val taskIds = communicationRepository.taskIdsByOrder(orderId).first() - val hasUnreadMessages = communicationRepository.hasUnreadPrescription(taskIds, orderId).first() - val prescriptions = if (withMedicationNames) { - taskIds.map { - communicationRepository.loadSyncedByTaskId(it).first()?.toPrescription() - ?: communicationRepository.loadScannedByTaskId(it).first()?.toPrescription() - } - } else { - emptyList() - } - - if (pharmacyName == null) { - communicationRepository.downloadMissingPharmacy(recipient) - } - - return toOrder( - prescriptions = prescriptions, - hasUnreadMessages = hasUnreadMessages, - taskIds = taskIds, - pharmacyName = pharmacyName - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrderUsingOrderIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrderUsingOrderIdUseCase.kt deleted file mode 100644 index 8f16208d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrderUsingOrderIdUseCase.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.orders.mappers.toOrderDetail -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.mapper.toPrescription -import de.gematik.ti.erp.app.prescription.model.Communication -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn - -class GetOrderUsingOrderIdUseCase( - private val communicationRepository: CommunicationRepository, - private val invoiceRepository: InvoiceRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - operator fun invoke(orderId: String): Flow = - combine( - communicationRepository.loadDispReqCommunications(orderId), - communicationRepository.loadPharmacies() - ) { communications, pharmacies -> - Napier.d("communications are $communications \npharmacies are $pharmacies ") - communications.firstOrNull()?.let { communication -> - communication.dispenseRequestCommunicationToOrder( - communicationRepository = communicationRepository, - withMedicationNames = true, - pharmacyName = pharmacies.find { it.telematikId == communication.recipient }?.name - ) - } - }.flowOn(dispatcher) - - private suspend fun Communication.dispenseRequestCommunicationToOrder( - communicationRepository: CommunicationRepository, - withMedicationNames: Boolean, - pharmacyName: String? - ): OrderUseCaseData.OrderDetail { - val taskIds = communicationRepository.taskIdsByOrder(orderId).first() - val hasUnreadMessages = communicationRepository.hasUnreadPrescription(taskIds, orderId).first() - - val taskDetailedBundles = taskIds.map { - val invoice: InvoiceData.PKVInvoice? = invoiceRepository.invoiceById(it).first() - OrderUseCaseData.TaskDetailedBundle( - taskId = it, - hasInvoice = invoice != null, - prescription = when { - withMedicationNames -> loadPrescription(it) - else -> null - } - ) - } - if (pharmacyName == null) { - communicationRepository.downloadMissingPharmacy(recipient) - } - - return toOrderDetail( - hasUnreadMessages = hasUnreadMessages, - taskDetailedBundles = taskDetailedBundles, - pharmacyName = pharmacyName - ) - } - - private suspend fun loadPrescription(taskId: String) = - communicationRepository.loadSyncedByTaskId(taskId).first()?.toPrescription() - ?: communicationRepository.loadScannedByTaskId(taskId).first()?.toPrescription() -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCase.kt deleted file mode 100644 index d3a2082b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetOrdersUsingProfileIdUseCase.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn - -class GetOrdersUsingProfileIdUseCase( - private val repository: CommunicationRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - operator fun invoke(profileIdentifier: ProfileIdentifier): Flow> = - combine( - repository.loadFirstDispReqCommunications(profileIdentifier), - repository.loadPharmacies() - ) { communications, pharmacies -> - communications.map { communication -> - communication.dispenseRequestCommunicationToOrder( - communicationRepository = repository, - withMedicationNames = false, - pharmacyName = pharmacies.find { it.telematikId == communication.recipient }?.name - ) - } - }.flowOn(dispatcher) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCase.kt deleted file mode 100644 index 282d4f85..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetRepliedMessagesUseCase.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.orders.mappers.toMessage -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map - -class GetRepliedMessagesUseCase( - private val communicationRepository: CommunicationRepository, - private val invoiceRepository: InvoiceRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - operator fun invoke(orderId: String): Flow> = - communicationRepository.taskIdsByOrder(orderId).flatMapLatest { taskIds -> - Napier.d("taskIds is $taskIds") - communicationRepository.loadRepliedCommunications(taskIds = taskIds) - .map { communications -> - Napier.d("communications is $communications") // Getting empty list if No reply message - communications.map { - val invoice: InvoiceData.PKVInvoice? = invoiceRepository.invoiceById( - it.taskId - ).first() // Loading chargeItem with TaskId - Napier.d("invoice is $invoice") - it.toMessage(invoice != null) - } - } - }.flowOn(dispatcher) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCase.kt deleted file mode 100644 index 76edb40e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/GetUnreadOrdersUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn - -class GetUnreadOrdersUseCase( - private val repository: CommunicationRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - operator fun invoke(profileId: ProfileIdentifier) = - repository.unreadOrders(profileId).flowOn(dispatcher) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCase.kt deleted file mode 100644 index 266c74a6..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/SaveLocalCommunicationUseCase.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class SaveLocalCommunicationUseCase( - private val repository: CommunicationRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - suspend operator fun invoke(taskId: String, pharmacyId: String, transactionId: String) { - withContext(dispatcher) { - repository.saveLocalCommunication(taskId, pharmacyId, transactionId) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCase.kt deleted file mode 100644 index 5ab02379..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByCommunicationIdUseCase.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class UpdateCommunicationByCommunicationIdUseCase( - private val repository: CommunicationRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - suspend operator fun invoke(communicationId: String) { - withContext(dispatcher) { - repository.setCommunicationStatus(communicationId, true) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCase.kt deleted file mode 100644 index f7009a47..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/UpdateCommunicationByOrderIdUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase - -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withContext - -class UpdateCommunicationByOrderIdUseCase( - private val repository: CommunicationRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - suspend operator fun invoke(orderId: String) { - withContext(dispatcher) { - repository.loadDispReqCommunications(orderId).first().forEach { - repository.setCommunicationStatus(it.communicationId, true) - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt deleted file mode 100644 index 542dff72..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.usecase.model - -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription -import kotlinx.datetime.Instant - -object OrderUseCaseData { - data class Pharmacy( - val id: String, - val name: String - ) - - data class Order( - val orderId: String, - // TODO: List is a duplicate here, we need to refactor it since it is inside the List - val taskIds: List, - val prescriptions: List, - val sentOn: Instant, - val pharmacy: Pharmacy, - val hasUnreadMessages: Boolean - ) - - data class OrderDetail( - val orderId: String, - val taskDetailedBundles: List, - val sentOn: Instant, - val pharmacy: Pharmacy, - val hasUnreadMessages: Boolean - ) - - data class TaskDetailedBundle( - // TODO: Task-id is a duplicate here, we need to refactor it - val taskId: String, - val hasInvoice: Boolean = false, - val prescription: Prescription? - ) - - data class Message( - val communicationId: String, - val sentOn: Instant, - val message: String?, - val code: String?, - val link: String?, - val consumed: Boolean, - val hasInvoice: Boolean - ) { - enum class Type { - All, - Link, - Code, - Text, - Empty - } - - val type: Type = run { - var filled = 0 - link?.let { filled++ } - code?.let { filled++ } - message?.let { filled++ } - - if (filled == 0) { - Type.Empty - } else if (filled > 1) { - Type.All - } else { - when { - link != null -> Type.Link - code != null -> Type.Code - message != null -> Type.Text - else -> Type.All - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/CameraPermission.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/CameraPermission.kt new file mode 100644 index 00000000..d3b623b5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/CameraPermission.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("TopLevelPropertyNaming") + +package de.gematik.ti.erp.app.permissions + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.Requirement +import java.io.ByteArrayOutputStream + +const val cameraPermission = android.Manifest.permission.CAMERA + +@Composable +fun getBitmapFromCamera( + onResult: (Bitmap) -> Unit +) = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicturePreview() +) { + it?.let { originalBitmap -> + onResult(removeMetadataFromBitmap(originalBitmap)) + } +} + +@Suppress("MagicNumber") +@Requirement( + "O.Data_8#2", + "O.Data_9#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Metadata is removed from the bitmap before it is stored for avatar image." +) +fun removeMetadataFromBitmap(originalBitmap: Bitmap): Bitmap { + // Compress the original bitmap to a byte array + val byteArrayOutputStream = ByteArrayOutputStream() + originalBitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + + // Decode the byte array back into a bitmap + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/LocationPermission.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/LocationPermission.kt new file mode 100644 index 00000000..73f76ac3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/LocationPermission.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.permissions + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.core.content.ContextCompat +import de.gematik.ti.erp.app.Requirement + +val locationPermissions = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION +) + +@Composable +fun getLocationPermissionLauncher( + onPermissionResult: (Boolean) -> Unit +) = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val isGranted = permissions.values.any { it } // if any permission is granted + onPermissionResult(isGranted) +} + +@Requirement( + "O.Plat_3#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "platform dialog for ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION" +) +fun Context.isLocationPermissionGranted(): Boolean = + locationPermissions.any { + ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED + } + +fun Context.isLocationServiceEnabled(): Boolean { + val manager = getSystemService(Context.LOCATION_SERVICE) as? LocationManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + manager?.isLocationEnabled ?: false + } else { + true + } +} + +fun Context.isLocationPermissionAndServiceEnabled(): Boolean = + isLocationPermissionGranted() && isLocationServiceEnabled() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/PermissionExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/PermissionExtension.kt new file mode 100644 index 00000000..53d142f0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/PermissionExtension.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.permissions + +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.shouldShowRationale + +@OptIn(ExperimentalPermissionsApi::class) +fun PermissionState.hasPermissionOrRationale() = status.isGranted || status.shouldShowRationale + +@OptIn(ExperimentalPermissionsApi::class) +fun PermissionState.denied() = status.isGranted.not() && status.shouldShowRationale.not() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/WriteExternalStoragePermission.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/WriteExternalStoragePermission.kt new file mode 100644 index 00000000..8e74042a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/permissions/WriteExternalStoragePermission.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.permissions + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.core.content.ContextCompat + +@Suppress("TopLevelPropertyNaming") +const val writeExternalStorage = Manifest.permission.WRITE_EXTERNAL_STORAGE + +fun Context.isWritePermissionGranted(): Boolean = ContextCompat.checkSelfPermission(this, writeExternalStorage) == PackageManager.PERMISSION_GRANTED + +@Composable +fun getWriteExternalStoragePermissionLauncher( + onPermissionResult: (Boolean) -> Unit +) = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isPermissionGranted -> + onPermissionResult(isPermissionGranted) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt index e82de6b9..6c0403f8 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt @@ -1,55 +1,96 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.di +import de.gematik.ti.erp.app.pharmacy.presentation.DefaultPharmacyGraphController import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController -import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyLocalDataSource import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyRepository -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRemoteDataSource +import de.gematik.ti.erp.app.pharmacy.repository.DefaultShippingContactRepository import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.PreviewMapCoordinatesRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.repository.datasource.DefaultFavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.DefaultOftenUsePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PharmacyRemoteDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PreviewMapCoordinatesDataSource +import de.gematik.ti.erp.app.pharmacy.ui.components.GooglePharmacyMap +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyMap +import de.gematik.ti.erp.app.pharmacy.usecase.ChangePharmacyFavoriteStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.DeleteOverviewPharmacyUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetLocationUseCase import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetPharmacyByTelematikIdUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetPreviewMapCoordinatesUseCase import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.IsPharmacyFavoriteUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.SaveShippingContactUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.SetPreviewMapCoordinatesUseCase +import de.gematik.ti.erp.app.redeem.repository.datasource.DefaultRedeemLocalDataSource +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource import org.kodein.di.DI import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton import org.kodein.di.instance val pharmacyModule = DI.Module("pharmacyModule", allowSilentOverride = true) { - bindProvider { PharmacyGraphController(instance(), instance(), instance()) } + bindSingleton { + DefaultPharmacyGraphController( + instance(), + instance(), + instance(), + instance(), + instance(), + instance() + ) + } bindProvider { GetShippingContactValidationUseCase() } - bindProvider { PharmacyDirectRedeemUseCase(instance()) } + bindProvider { PharmacyDirectRedeemUseCase(instance()) } // TODO: Only used in debug process on testing bindProvider { PharmacyMapsUseCase(instance(), instance(), instance()) } bindProvider { PharmacySearchUseCase(instance(), instance(), instance(), instance(), instance()) } bindProvider { PharmacyOverviewUseCase(instance(), instance()) } bindProvider { GetOverviewPharmaciesUseCase(instance()) } bindProvider { GetOrderStateUseCase(instance(), instance(), instance()) } + bindProvider { GetLocationUseCase(instance()) } + bindProvider { GetPreviewMapCoordinatesUseCase(instance()) } + bindProvider { SetPreviewMapCoordinatesUseCase(instance()) } + bindProvider { IsPharmacyFavoriteUseCase(instance()) } + bindProvider { ChangePharmacyFavoriteStateUseCase(instance()) } + bindProvider { SaveShippingContactUseCase(instance()) } + bindProvider { GetPharmacyByTelematikIdUseCase(instance()) } + bindProvider { DeleteOverviewPharmacyUseCase(instance()) } + bindProvider { GooglePharmacyMap() } } val pharmacyRepositoryModule = DI.Module("pharmacyRepositoryModule", allowSilentOverride = true) { bindProvider { PharmacyRemoteDataSource(instance(), instance()) } - bindProvider { DefaultPharmacyRepository(instance(), instance(), instance()) } - bindProvider { ShippingContactRepository(instance(), instance()) } - bindProvider { DefaultPharmacyLocalDataSource(instance()) } + bindProvider { DefaultRedeemLocalDataSource(instance()) } + bindProvider { DefaultFavouritePharmacyLocalDataSource(instance()) } + bindProvider { DefaultOftenUsePharmacyLocalDataSource(instance()) } + bindSingleton { PreviewMapCoordinatesDataSource() } + bindProvider { PreviewMapCoordinatesRepository(instance()) } + bindProvider { DefaultPharmacyRepository(instance(), instance(), instance(), instance()) } + bindProvider { DefaultShippingContactRepository(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PharmacyScreenDataMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PharmacyScreenDataMapper.kt new file mode 100644 index 00000000..ca2464b7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PharmacyScreenDataMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.mapper + +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.repository.RemoteRedeemOption + +internal fun PharmacyScreenData.OrderOption.toRedeemOption(): RemoteRedeemOption = + when (this) { + PharmacyScreenData.OrderOption.PickupService -> RemoteRedeemOption.Local + PharmacyScreenData.OrderOption.CourierDelivery -> RemoteRedeemOption.Delivery + PharmacyScreenData.OrderOption.MailDelivery -> RemoteRedeemOption.Shipment + } + +internal fun PharmacyScreenData.OrderOption.toPharmacyContact(data: PharmacyUseCaseData.Pharmacy): String = + when (this) { + PharmacyScreenData.OrderOption.PickupService -> data.contacts.pickUpUrl + PharmacyScreenData.OrderOption.CourierDelivery -> data.contacts.deliveryUrl + PharmacyScreenData.OrderOption.MailDelivery -> data.contacts.onlineServiceUrl + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt index 70fe2c6e..b7ef5c3d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.mapper @@ -26,6 +26,8 @@ fun ScannedTaskData.ScannedTask.toOrder() = PharmacyUseCaseData.PrescriptionOrder( taskId = taskId, accessCode = accessCode, + isSelfPayerPrescription = false, + isScanned = true, title = name, index = index, timestamp = scannedOn, @@ -35,9 +37,12 @@ fun ScannedTaskData.ScannedTask.toOrder() = fun SyncedTaskData.SyncedTask.toOrder() = PharmacyUseCaseData.PrescriptionOrder( taskId = taskId, - accessCode = accessCode!!, // TODO: check, why we get here a nullable!! + accessCode = accessCode, + isSelfPayerPrescription = insuranceInformation + .coverageType == SyncedTaskData.CoverageType.SEL, title = medicationName(), index = null, timestamp = authoredOn, - substitutionsAllowed = false + substitutionsAllowed = false, + isScanned = false ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/PrescriptionRedeemArguments.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/PrescriptionRedeemArguments.kt new file mode 100644 index 00000000..aa838347 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/PrescriptionRedeemArguments.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.model + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import java.util.UUID +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +internal fun orderID(): UUID = UUID.randomUUID() + +sealed class PrescriptionRedeemArguments( + open val profile: ProfilesUseCaseData.Profile? = null, + open val orderId: UUID, + open val prescriptionOrderInfos: List, + open val redeemOption: PharmacyScreenData.OrderOption, + open val pharmacy: PharmacyUseCaseData.Pharmacy, + open val contact: PharmacyUseCaseData.ShippingContact +) { + @OptIn(ExperimentalContracts::class) + fun onRedemptionState( + directRedemptionBlock: (DirectRedemptionArguments) -> Unit, + loggedInUserRedemptionBlock: (LoggedInUserRedemptionArguments) -> Unit + ) { + contract { + returns(true) implies (this@PrescriptionRedeemArguments is DirectRedemptionArguments) + returns(false) implies (this@PrescriptionRedeemArguments is LoggedInUserRedemptionArguments) + } + when { + profile?.isDirectRedeemEnabled == true || profile == null -> directRedemptionBlock(this as DirectRedemptionArguments) + else -> loggedInUserRedemptionBlock(this as LoggedInUserRedemptionArguments) + } + } + + // arguments required to redeem a prescription for a logged in user + data class LoggedInUserRedemptionArguments( + override val profile: ProfilesUseCaseData.Profile, + override val orderId: UUID, + override val prescriptionOrderInfos: List, + override val redeemOption: PharmacyScreenData.OrderOption, + override val pharmacy: PharmacyUseCaseData.Pharmacy, + override val contact: PharmacyUseCaseData.ShippingContact + ) : PrescriptionRedeemArguments(profile, orderId, prescriptionOrderInfos, redeemOption, pharmacy, contact) + + // arguments required to redeem a prescription for a first time user who has never logged in + data class DirectRedemptionArguments( + override val orderId: UUID, + override val prescriptionOrderInfos: List, + override val redeemOption: PharmacyScreenData.OrderOption, + override val pharmacy: PharmacyUseCaseData.Pharmacy, + override val contact: PharmacyUseCaseData.ShippingContact + ) : PrescriptionRedeemArguments(null, orderId, prescriptionOrderInfos, redeemOption, pharmacy, contact) + + companion object { + fun UUID.from( + profile: ProfilesUseCaseData.Profile, + order: PharmacyUseCaseData.OrderState, + redeemOption: PharmacyScreenData.OrderOption, + pharmacy: PharmacyUseCaseData.Pharmacy + ): PrescriptionRedeemArguments = + when { + profile.isDirectRedeemEnabled -> { + DirectRedemptionArguments( + orderId = this, + prescriptionOrderInfos = order.prescriptionOrders, + redeemOption = redeemOption, + pharmacy = pharmacy, + contact = order.contact + ) + } + + else -> { + LoggedInUserRedemptionArguments( + profile = profile, + orderId = this, + prescriptionOrderInfos = order.prescriptionOrders, + redeemOption = redeemOption, + pharmacy = pharmacy, + contact = order.contact + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/SelectedFavouritePharmacyState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/SelectedFavouritePharmacyState.kt new file mode 100644 index 00000000..c8e6803c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/model/SelectedFavouritePharmacyState.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.model + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData + +sealed interface SelectedFavouritePharmacyState { + data object Idle : SelectedFavouritePharmacyState + data object Loading : SelectedFavouritePharmacyState + data class Error(val throwable: Throwable) : SelectedFavouritePharmacyState + data object Missing : SelectedFavouritePharmacyState + data class Data(val pharmacy: PharmacyUseCaseData.Pharmacy) : SelectedFavouritePharmacyState +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyGraph.kt index f93ab2e7..dd6cae45 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.navigation @@ -23,15 +23,21 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import de.gematik.ti.erp.app.navigation.renderBottomSheet import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideOutUp import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyFilterSheetScreen -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyStartScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacyDetailsFromMessageScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacyDetailsFromPharmacyScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacyFilterSheetScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacySearchListScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacySearchMapsScreen +import de.gematik.ti.erp.app.pharmacy.ui.screens.PharmacyStartScreen import org.kodein.di.DI import org.kodein.di.instance fun NavGraphBuilder.pharmacyGraph( dependencyInjector: DI, - startDestination: String = PharmacyRoutes.PharmacyStartScreen.route, + startDestination: String = PharmacyRoutes.PharmacyStartScreen.path(""), navController: NavController ) { val controller by dependencyInjector.instance() @@ -41,6 +47,9 @@ fun NavGraphBuilder.pharmacyGraph( route = PharmacyRoutes.subGraphName() ) { renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, route = PharmacyRoutes.PharmacyStartScreen.route, arguments = PharmacyRoutes.PharmacyStartScreen.arguments ) { navEntry -> @@ -50,7 +59,18 @@ fun NavGraphBuilder.pharmacyGraph( graphController = controller ) } - + // same screen as above, it is duplicated for the navigation graph so that it is called + // in a modal flow and bottom sheet is not shown + renderComposable( + route = PharmacyRoutes.PharmacyStartScreenModal.route, + arguments = PharmacyRoutes.PharmacyStartScreenModal.arguments + ) { navEntry -> + PharmacyStartScreen( + navController = navController, + navBackStackEntry = navEntry, + graphController = controller + ) + } renderBottomSheet( route = PharmacyRoutes.PharmacyFilterSheetScreen.route, arguments = PharmacyRoutes.PharmacyFilterSheetScreen.arguments @@ -61,10 +81,44 @@ fun NavGraphBuilder.pharmacyGraph( graphController = controller ) } - // TODO: PharmacySearchListScreen - // TODO: PharmacySearchMapsScreen - // TODO: PharmacyOrderOverviewScreen - // TODO: PharmacyEditShippingContactScreen - // TODO: PharmacyPrescriptionSelectionScreen + renderComposable( + route = PharmacyRoutes.PharmacySearchListScreen.route, + arguments = PharmacyRoutes.PharmacySearchListScreen.arguments + ) { + PharmacySearchListScreen( + navController = navController, + navBackStackEntry = it, + graphController = controller + ) + } + renderComposable( + route = PharmacyRoutes.PharmacySearchMapsScreen.route, + arguments = PharmacyRoutes.PharmacySearchMapsScreen.arguments + ) { + PharmacySearchMapsScreen( + navController = navController, + navBackStackEntry = it, + graphController = controller + ) + } + renderBottomSheet( + route = PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.route, + arguments = PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.arguments + ) { + PharmacyDetailsFromPharmacyScreen( + navController = navController, + navBackStackEntry = it, + graphController = controller + ) + } + renderBottomSheet( + route = PharmacyRoutes.PharmacyDetailsFromMessageScreen.route, + arguments = PharmacyRoutes.PharmacyDetailsFromMessageScreen.arguments + ) { + PharmacyDetailsFromMessageScreen( + navController = navController, + navBackStackEntry = it + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyRoutes.kt index d5f1d41d..6073d8bc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/navigation/PharmacyRoutes.kt @@ -1,48 +1,134 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.navigation +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavType import androidx.navigation.navArgument import de.gematik.ti.erp.app.navigation.NavigationRouteNames import de.gematik.ti.erp.app.navigation.NavigationRoutes import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.navigation.fromNavigationString +import de.gematik.ti.erp.app.navigation.toNavigationString +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.EMPTY_TASK_ID +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData object PharmacyRoutes : NavigationRoutes { override fun subGraphName(): String = "pharmacies" - const val NearByFilterArgument = "nearByFilter" - const val ShowStartSearchButton = "showStartSearchButton" + const val EMPTY_TASK_ID = "" + + const val PHARMACY_NAV_NEARBY_FILTER = "nearByFilter" + const val PHARMACY_NAV_WITH_START_BUTTON = "showStartSearchButton" + const val PHARMACY_NAV_SELECTED_PHARMACY = "Pharmacy" + const val PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN = "ShowBackOnStartScreen" + const val PHARMACY_NAV_TASK_ID = "taskId" + + object PharmacyStartScreen : Routes( + path = NavigationRouteNames.PharmacyStartScreen.name, + navArgument(PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN) { type = NavType.BoolType }, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = + path( + PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN to false, + PHARMACY_NAV_TASK_ID to taskId + ) + } + + object PharmacyStartScreenModal : Routes( + path = NavigationRouteNames.PharmacyStartScreenModal.name, + navArgument(PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN) { type = NavType.BoolType }, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = + path( + PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN to true, + PHARMACY_NAV_TASK_ID to taskId + ) + } - object PharmacyStartScreen : Routes(NavigationRouteNames.PharmacyStartScreen.name) object PharmacyFilterSheetScreen : Routes( path = NavigationRouteNames.PharmacyFilterSheetScreen.name, - navArgument(NearByFilterArgument) { type = NavType.BoolType }, - navArgument(ShowStartSearchButton) { type = NavType.BoolType } + navArgument(PHARMACY_NAV_NEARBY_FILTER) { type = NavType.BoolType }, + navArgument(PHARMACY_NAV_WITH_START_BUTTON) { type = NavType.BoolType } + ) { + fun path(showNearbyFilter: Boolean, navigateWithSearchButton: Boolean) = + path(PHARMACY_NAV_NEARBY_FILTER to showNearbyFilter, PHARMACY_NAV_WITH_START_BUTTON to navigateWithSearchButton) + } + + object PharmacySearchListScreen : Routes( + NavigationRouteNames.PharmacySearchListScreen.name, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = + path( + PHARMACY_NAV_TASK_ID to taskId + ) + } + + object PharmacySearchMapsScreen : Routes( + NavigationRouteNames.PharmacySearchMapsScreen.name, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = + path( + PHARMACY_NAV_TASK_ID to taskId + ) + } + + object PharmacyDetailsFromPharmacyScreen : Routes( + path = NavigationRouteNames.PharmacyDetailsFromMessageScreen.name, + navArgument(PHARMACY_NAV_SELECTED_PHARMACY) { type = NavType.StringType }, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(showNearbyFilter: Boolean, showButton: Boolean) = - path(NearByFilterArgument to showNearbyFilter, ShowStartSearchButton to showButton) + fun path(pharmacy: PharmacyUseCaseData.Pharmacy, taskId: String): String = + PharmacyDetailsFromPharmacyScreen.path( + PHARMACY_NAV_SELECTED_PHARMACY to pharmacy.toNavigationString(), + PHARMACY_NAV_TASK_ID to taskId + ) } - object PharmacySearchListScreen : Routes(NavigationRouteNames.PharmacySearchListScreen.name) - object PharmacySearchMapsScreen : Routes(NavigationRouteNames.PharmacySearchMapsScreen.name) - object PharmacyOrderOverviewScreen : Routes(NavigationRouteNames.PharmacyOrderOverviewScreen.name) - object PharmacyEditShippingContactScreen : Routes(NavigationRouteNames.PharmacyEditShippingContactScreen.name) - object PharmacyPrescriptionSelectionScreen : Routes(NavigationRouteNames.PharmacyPrescriptionSelectionScreen.name) + + object PharmacyDetailsFromMessageScreen : Routes( + path = NavigationRouteNames.PharmacyDetailsFromPharmacyScreen.name, + navArgument(PHARMACY_NAV_SELECTED_PHARMACY) { type = NavType.StringType }, + navArgument(PHARMACY_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(pharmacy: PharmacyUseCaseData.Pharmacy, taskId: String): String = + PharmacyDetailsFromMessageScreen.path( + PHARMACY_NAV_SELECTED_PHARMACY to pharmacy.toNavigationString(), + PHARMACY_NAV_TASK_ID to taskId + ) + } +} + +class PharmacyRouteBackStackEntryArguments( + private val navBackStackEntry: NavBackStackEntry +) { + internal fun getPharmacy(): PharmacyUseCaseData.Pharmacy? = + navBackStackEntry.arguments?.let { bundle -> + bundle.getString(PharmacyRoutes.PHARMACY_NAV_SELECTED_PHARMACY)?.let { + fromNavigationString(it) + } + } + + internal fun getTaskId(): String = + navBackStackEntry.arguments?.getString(PharmacyRoutes.PHARMACY_NAV_TASK_ID) ?: EMPTY_TASK_ID } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/FilterType.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/FilterType.kt new file mode 100644 index 00000000..dc98ca5b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/FilterType.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.serialization.Serializable + +@Serializable +enum class FilterType { + NEARBY, + OPEN_NOW, + DELIVERY_SERVICE, + ONLINE_SERVICE, + DIRECT_REDEEM; + + companion object { + fun FilterType.getUpdatedFilter(filter: PharmacyUseCaseData.Filter) = + when (this) { + NEARBY -> filter.copy(nearBy = !filter.nearBy) + OPEN_NOW -> filter.copy(openNow = !filter.openNow) + DELIVERY_SERVICE -> filter.copy(deliveryService = !filter.deliveryService) + ONLINE_SERVICE -> filter.copy(onlineService = !filter.onlineService) + DIRECT_REDEEM -> filter.copy(directRedeem = !filter.directRedeem) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt deleted file mode 100644 index ca8a391b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import kotlinx.coroutines.flow.Flow -import org.kodein.di.compose.rememberInstance - -@Stable -class PharmacyController( - private val pharmacyRepository: PharmacyRepository -) { - fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = - pharmacyRepository.isPharmacyInFavorites(pharmacy) - - suspend fun markPharmacyAsFavorite(pharmacy: PharmacyUseCaseData.Pharmacy) { - pharmacyRepository.saveOrUpdateFavoritePharmacy(pharmacy) - } - - suspend fun unmarkPharmacyAsFavorite(pharmacy: PharmacyUseCaseData.Pharmacy) { - pharmacyRepository.deleteFavoritePharmacy(pharmacy) - } -} - -@Composable -fun rememberPharmacyController(): PharmacyController { - val pharmacyRepository by rememberInstance() - return remember { - PharmacyController(pharmacyRepository) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyDetailsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyDetailsController.kt new file mode 100644 index 00000000..142ede65 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyDetailsController.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.pharmacy.usecase.ChangePharmacyFavoriteStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.IsPharmacyFavoriteUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class PharmacyDetailsController( + + private val isPharmacyFavoriteUseCase: IsPharmacyFavoriteUseCase, + private val changePharmacyFavoriteStateUseCase: ChangePharmacyFavoriteStateUseCase +) : Controller() { + + private val isPharmacyFavorite = MutableStateFlow(false) + + fun isPharmacyFavorite(pharmacy: PharmacyUseCaseData.Pharmacy) { + run { + controllerScope.launch { + isPharmacyFavoriteUseCase(pharmacy).collectLatest { + isPharmacyFavorite.value = it + } + } + } + } + + fun changePharmacyAsFavorite(pharmacy: PharmacyUseCaseData.Pharmacy, state: Boolean) { + controllerScope.launch { + changePharmacyFavoriteStateUseCase(pharmacy, state) + isPharmacyFavorite.value = state + } + } + + val isPharmacyFavoriteState + @Composable + get() = isPharmacyFavorite.collectAsStateWithLifecycle() +} + +@Composable +internal fun rememberPharmacyDetailsController(): PharmacyDetailsController { + val isPharmacyFavoriteUseCase by rememberInstance() + val changePharmacyFavoriteStateUseCase by rememberInstance() + + return remember { + PharmacyDetailsController( + isPharmacyFavoriteUseCase = isPharmacyFavoriteUseCase, + changePharmacyFavoriteStateUseCase = changePharmacyFavoriteStateUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyExtensions.kt new file mode 100644 index 00000000..aa4ac0c9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyExtensions.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import com.google.android.gms.maps.model.LatLng +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.fhir.model.PharmacyService +import de.gematik.ti.erp.app.fhir.model.isOpenAt +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +internal const val WILDCARD = "" +internal fun PharmacyUseCaseData.Pharmacy.location(locationMode: PharmacyUseCaseData.LocationMode) = + when (locationMode) { + is PharmacyUseCaseData.LocationMode.Enabled -> copy( + distance = coordinates?.minus(locationMode.coordinates) + ) + else -> this + } + +internal fun PharmacyUseCaseData.Pharmacy.deliveryService(isDeliveryServiceFiltered: Boolean) = + when { + isDeliveryServiceFiltered -> provides.any { it is PharmacyService.DeliveryPharmacyService } + else -> true + } + +internal fun PharmacyUseCaseData.Pharmacy.isOpenNow(isOpenNow: Boolean) = + if (isOpenNow) { + openingHours?.isOpenAt( + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + ) ?: false + } else { + true + } + +internal fun Coordinates.toLatLng() = LatLng(latitude, longitude) + +internal fun PharmacyUseCaseData.LocationMode.Enabled.toLatLng() = coordinates.toLatLng() + +internal fun LatLng.toCoordinates() = Coordinates(latitude, longitude) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyGraphController.kt index 65dd6507..6b0ec786 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyGraphController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyGraphController.kt @@ -1,30 +1,46 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.presentation +import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.State +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.base.SharedController +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.permissions.isLocationPermissionAndServiceEnabled +import de.gematik.ti.erp.app.permissions.isLocationServiceEnabled +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType.Companion.getUpdatedFilter +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PreviewMapCoordinatesDataSource.Companion.berlinCoordinates +import de.gematik.ti.erp.app.pharmacy.usecase.GetLocationUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetLocationUseCase.LocationResult import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetPreviewMapCoordinatesUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.SetPreviewMapCoordinatesUseCase import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -36,46 +52,141 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch -class PharmacyGraphController( +abstract class PharmacyGraphController : Controller() { + abstract val permissionDeniedEvent: ComposableEvent + abstract val serviceDisabledEvent: ComposableEvent + abstract val locationNotFoundEvent: ComposableEvent + abstract val askLocationPermissionEvent: ComposableEvent + abstract val locationProvidedEvent: ComposableEvent + + abstract fun init(context: Context) + abstract fun updateFilter(type: FilterType, clearLocation: Boolean = false) + abstract fun onLocationPermissionResult(isLocationGranted: Boolean) + abstract fun forceLocationFalse() + abstract fun forceLocationFalseWithSelectedCoordinates(selectedCoordinates: Coordinates) + abstract fun checkLocationServiceAndPermission(context: Context) + + @Composable + abstract fun filter(): State + + @Composable + abstract fun coordinates(): State + + @Composable + abstract fun favouritePharmacies(): State> + + @Composable + abstract fun previewCoordinates(): State + + @Composable + abstract fun hasRedeemableOrders(): State + + @Composable + abstract fun isDirectRedeemEnabled(): State + + abstract fun reset() +} + +class DefaultPharmacyGraphController( private val getActiveProfileUseCase: GetActiveProfileUseCase, private val getOverviewPharmaciesUseCase: GetOverviewPharmaciesUseCase, - private val getOrderStateUseCase: GetOrderStateUseCase -) : SharedController() { + private val getOrderStateUseCase: GetOrderStateUseCase, + private val getLocationUseCase: GetLocationUseCase, + private val setPreviewMapCoordinatesUseCase: SetPreviewMapCoordinatesUseCase, + private val getPreviewMapCoordinatesUseCase: GetPreviewMapCoordinatesUseCase +) : PharmacyGraphController() { + + private val _coordinates = MutableStateFlow(null) - private val activeProfile by lazy { + private val _activeProfile by lazy { getActiveProfileUseCase() .stateIn(controllerScope, SharingStarted.WhileSubscribed(0, 0), null) } - private val orders by lazy { getOrderStateUseCase() } + private val _orderState by lazy { getOrderStateUseCase() } @OptIn(ExperimentalCoroutinesApi::class) - private val hasRedeemableOrders by lazy { - orders.map { it.orders.isNotEmpty() }.mapLatest { return@mapLatest it } + private val _hasRedeemableOrders by lazy { + _orderState.map { it.prescriptionOrders.isNotEmpty() }.mapLatest { return@mapLatest it } } - private val filter = MutableStateFlow(PharmacyUseCaseData.Filter()) + private val _filter = MutableStateFlow(PharmacyUseCaseData.Filter()) - private val isDirectRedeemEnabled = activeProfile + private val _isDirectRedeemEnabled = _activeProfile .mapNotNull { (it?.lastAuthenticated == null) } - private val favouritePharmacies by lazy { + private val _favouritePharmacies by lazy { getOverviewPharmaciesUseCase().stateIn(controllerScope, SharingStarted.Lazily, emptyList()) } + private fun onPermissionDenied() { + forceLocationFalse() + permissionDeniedEvent.trigger() + } + + private fun onLocationNotFound() { + forceLocationFalse() + locationNotFoundEvent.trigger() + } + + private fun onLocationServiceDisabled() { + forceLocationFalse() + serviceDisabledEvent.trigger() + } + + @ReadOnlyComposable + @Composable + @Suppress("ComposableNaming") + private fun Context.resetPreviewOnLocationNotAllowed() { + val isDenied = isLocationPermissionAndServiceEnabled().not() + if (isDenied) { + setPreviewMapCoordinatesUseCase.invoke(null) + } + } + init { - init() + updateIsDirectRedeemEnabledOnFilter() + } + + // Location events + override val permissionDeniedEvent = ComposableEvent() + + override val serviceDisabledEvent = ComposableEvent() + + override val locationNotFoundEvent = ComposableEvent() + + override val askLocationPermissionEvent = ComposableEvent() + + override val locationProvidedEvent = ComposableEvent() + + override fun init(context: Context) { + updateIsDirectRedeemEnabledOnFilter() + updateLocation(context) + _filter.value = PharmacyUseCaseData.Filter() } - fun init() { + private fun updateLocation(context: Context) { controllerScope.launch { - filter.value = PharmacyUseCaseData.Filter() + if (context.isLocationPermissionAndServiceEnabled()) { + runCatching { + getLocationUseCase() + }.onSuccess { result -> + if (result is LocationResult.Success) { + val coordinatesResult = Coordinates( + latitude = result.location.latitude, + longitude = result.location.longitude + ) + _coordinates.value = coordinatesResult + setPreviewMapCoordinatesUseCase.invoke(coordinatesResult) + } + } + } } } - fun updateIsDirectRedeemEnabledOnFilter() { + private fun updateIsDirectRedeemEnabledOnFilter() { controllerScope.launch { - combine(hasRedeemableOrders, isDirectRedeemEnabled) { hasOrders, isEnabled -> + combine(_hasRedeemableOrders, _isDirectRedeemEnabled) { hasOrders, isEnabled -> if (hasOrders && isEnabled) { updateFilter(type = FilterType.DIRECT_REDEEM) } @@ -83,35 +194,94 @@ class PharmacyGraphController( } } - fun updateFilter(type: FilterType) { - when (type) { - FilterType.NEARBY -> filter.value = filter.updateAndGet { it.copy(nearBy = !it.nearBy) } - FilterType.OPEN_NOW -> filter.value = filter.updateAndGet { it.copy(openNow = !it.openNow) } - FilterType.DELIVERY_SERVICE -> - filter.value = - filter.updateAndGet { it.copy(deliveryService = !it.deliveryService) } + override fun onCleared() { + super.onCleared() + reset() + } + + override fun updateFilter(type: FilterType, clearLocation: Boolean) { + _filter.value = _filter.updateAndGet { type.getUpdatedFilter(_filter.value) } + if (clearLocation) { + _coordinates.value = null + } + } + + override fun forceLocationFalse() { + _filter.value = _filter.value.copy(nearBy = false) + setPreviewMapCoordinatesUseCase.invoke(null) + _coordinates.value = null + } + + override fun forceLocationFalseWithSelectedCoordinates(selectedCoordinates: Coordinates) { + _filter.value = _filter.value.copy(nearBy = false) + setPreviewMapCoordinatesUseCase.invoke(selectedCoordinates) + _coordinates.value = selectedCoordinates + } - FilterType.ONLINE_SERVICE -> - filter.value = - filter.updateAndGet { it.copy(onlineService = !it.onlineService) } + override fun checkLocationServiceAndPermission(context: Context) { + val isLocationServiceEnabled = context.isLocationServiceEnabled() + if (isLocationServiceEnabled) { + askLocationPermissionEvent.trigger() + } else { + onLocationServiceDisabled() + } + } - FilterType.DIRECT_REDEEM -> filter.value = filter.updateAndGet { it.copy(directRedeem = !it.directRedeem) } + override fun onLocationPermissionResult(isLocationGranted: Boolean) { + controllerScope.launch { + if (isLocationGranted) { + runCatching { + getLocationUseCase() + }.onSuccess { result -> + when (result) { + LocationResult.LocationNotFound -> onLocationNotFound() + LocationResult.PermissionDenied -> onPermissionDenied() + LocationResult.ServiceDisabled -> onLocationServiceDisabled() + is LocationResult.Success -> { + val coordinatesResult = Coordinates( + latitude = result.location.latitude, + longitude = result.location.longitude + ) + _coordinates.value = coordinatesResult + setPreviewMapCoordinatesUseCase.invoke(coordinatesResult) + _filter.value = _filter.value.copy(nearBy = true) + locationProvidedEvent.trigger() + } + } + }.onFailure { + onLocationNotFound() + } + } else { + onPermissionDenied() + } } } - val filterState - @Composable - get() = filter.collectAsStateWithLifecycle(PharmacyUseCaseData.Filter()) + override fun reset() { + _filter.value = PharmacyUseCaseData.Filter() + } + + @Composable + override fun filter(): State { + return _filter.collectAsStateWithLifecycle(PharmacyUseCaseData.Filter()) + } - val favouritePharmaciesState - @Composable - get() = favouritePharmacies.collectAsStateWithLifecycle(emptyList()) + @Composable + override fun coordinates() = _coordinates.collectAsStateWithLifecycle(null) - enum class FilterType { - NEARBY, - OPEN_NOW, - DELIVERY_SERVICE, - ONLINE_SERVICE, - DIRECT_REDEEM + @Composable + override fun favouritePharmacies() = _favouritePharmacies.collectAsStateWithLifecycle(emptyList()) + + @Composable + override fun previewCoordinates(): State { + LocalContext.current.resetPreviewOnLocationNotAllowed() + return getPreviewMapCoordinatesUseCase() + .collectAsStateWithLifecycle(berlinCoordinates) } + + @Composable + override fun isDirectRedeemEnabled() = _isDirectRedeemEnabled.collectAsStateWithLifecycle(false) + + @Composable + override fun hasRedeemableOrders() = _hasRedeemableOrders.collectAsStateWithLifecycle(false) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt deleted file mode 100644 index 9b229e7c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.presentation - -import androidx.annotation.RestrictTo -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.profiles.presentation.ProfileController.Companion.DEFAULT_EMPTY_PROFILE -import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -@Stable -class PharmacyOrderController( - private val getActiveProfileUseCase: GetActiveProfileUseCase, - private val pharmacySearchUseCase: PharmacySearchUseCase, - private val getOrderStateUseCase: GetOrderStateUseCase, - private val getShippingContactValidationUseCase: GetShippingContactValidationUseCase, - private val scope: CoroutineScope -) { - private val activeProfile by lazy { - getActiveProfileUseCase().stateIn(scope, SharingStarted.WhileSubscribed(0, 0), DEFAULT_EMPTY_PROFILE) - } - - private val isDirectRedeemEnabled = activeProfile.mapNotNull { it.lastAuthenticated == null } - - private val orders by lazy { getOrderStateUseCase() } - - @OptIn(ExperimentalCoroutinesApi::class) - private val hasRedeemableOrders by lazy { - orders.map { it.orders.isNotEmpty() }.mapLatest { return@mapLatest it } - } - - private val prescription by lazy { orders.map { it.orders } } - - private val unSelectedPrescriptions: MutableStateFlow> = MutableStateFlow(emptyList()) - - private val updatedOrders = - combine( - unSelectedPrescriptions, - orders - ) { unSelectedPrescriptions, prescriptionOrder -> - prescriptionOrder.copy( - orders = prescriptionOrder.orders.filter { it.taskId !in unSelectedPrescriptions } - ) - } - - val activeProfileState - @Composable - get() = activeProfile.collectAsStateWithLifecycle() - - val isDirectRedeemEnabledState - @Composable - get() = isDirectRedeemEnabled.collectAsStateWithLifecycle(false) - - val hasRedeemableOrdersState - @Composable - get() = hasRedeemableOrders.collectAsStateWithLifecycle(false) - - val orderState - @Composable - get() = updatedOrders.collectAsStateWithLifecycle(PharmacyUseCaseData.OrderState.Empty) - - val prescriptionsState - @Composable - get() = prescription.collectAsStateWithLifecycle(emptyList()) - - var selectedPharmacy: PharmacyUseCaseData.Pharmacy? by mutableStateOf(null) - private set - - var selectedOrderOption: PharmacyScreenData.OrderOption? by mutableStateOf(null) - private set - - fun onSelectPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy, orderOption: PharmacyScreenData.OrderOption) { - selectedPharmacy = pharmacy - selectedOrderOption = orderOption - } - - fun onSelectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { - unSelectedPrescriptions.update { it - order.taskId } - } - - fun onDeselectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { - unSelectedPrescriptions.update { it + order.taskId } - } - - fun onSaveContact(contact: PharmacyUseCaseData.ShippingContact) { - scope.launch { - pharmacySearchUseCase.saveShippingContact(contact) - } - } - - fun onResetPharmacySelection() { - selectedPharmacy = null - selectedOrderOption = null - } - - fun onResetPrescriptionSelection() { - unSelectedPrescriptions.value = emptyList() - } - - fun shippingContactState( - contact: PharmacyUseCaseData.ShippingContact, - selectedOrderOption: PharmacyScreenData.OrderOption - ): ShippingContactState = getShippingContactValidationUseCase(contact, selectedOrderOption) - - @RestrictTo(RestrictTo.Scope.TESTS) - val updatedOrdersForTest = updatedOrders -} - -@Composable -fun rememberPharmacyOrderController(): PharmacyOrderController { - val getActiveProfileUseCase by rememberInstance() - val pharmacySearchUseCase by rememberInstance() - val getOrderStateUseCase by rememberInstance() - val getShippingContactValidationUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - return remember { - PharmacyOrderController( - getActiveProfileUseCase = getActiveProfileUseCase, - pharmacySearchUseCase = pharmacySearchUseCase, - getOrderStateUseCase = getOrderStateUseCase, - getShippingContactValidationUseCase = getShippingContactValidationUseCase, - scope = scope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt index 1934decf..5a7d935a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.presentation @@ -43,8 +43,8 @@ import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationServices import com.google.android.gms.tasks.CancellationTokenSource import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService -import de.gematik.ti.erp.app.fhir.model.Location +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.fhir.model.PharmacyService import de.gematik.ti.erp.app.fhir.model.isOpenAt import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase @@ -53,8 +53,8 @@ import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.LocationMode -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions +import de.gematik.ti.erp.app.api.ErpServiceState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -82,7 +82,7 @@ import kotlinx.datetime.toLocalDateTime import org.kodein.di.compose.rememberInstance private const val WaitForLocationUpdate = 2500L -private const val DefaultRadiusInMeter = 999 * 1000.0 +internal const val DefaultRadiusInMeter = 999 * 1000.0 @Stable class PharmacySearchController( @@ -103,7 +103,7 @@ class PharmacySearchController( get() = overviewPharmacies.collectAsStateWithLifecycle() @Stable - sealed interface State : PrescriptionServiceState { + sealed interface State : ErpServiceState { @Stable data object Loading : State @@ -150,7 +150,7 @@ class PharmacySearchController( ) @OptIn(ExperimentalCoroutinesApi::class) - val pharmacyMapsFlow: Flow = + val pharmacyMapsFlow: Flow = searchChannelFlow .filterNotNull() .onEach { @@ -160,10 +160,26 @@ class PharmacySearchController( flow { emit(State.Loading) - val pharmacies = pharmacyMapsUseCase.searchPharmacies(searchData) + val pharmacies = pharmacyMapsUseCase.invoke( + PharmacyUseCaseData.MapsSearchData( + name = searchData.name, + filter = searchData.filter, + locationMode = searchData.locationMode, + coordinates = searchData.locationMode.let { + if (it is LocationMode.Enabled) { + it.coordinates + } else { + null + } + } + ) + ) pharmacies - .map { it.updateDistanceForEnabledLocation(searchData.locationMode) } + .map { + + it.updateDistanceForEnabledLocation(searchData.locationMode) + } .filter { it.providesDeliveryService(searchData.filter.deliveryService) } .filter { it.hasOpeningHours(searchData.filter.openNow) } .also { @@ -188,10 +204,11 @@ class PharmacySearchController( private val lock = Mutex() + @Suppress("CyclomaticComplexMethod") suspend fun search( name: String, filter: PharmacyUseCaseData.Filter, - location: Location? = null, + coordinates: Coordinates? = null, radiusInMeter: Double = DefaultRadiusInMeter ): SearchQueryResult = withContext(Dispatchers.IO) { lock.withLock { @@ -202,7 +219,7 @@ class PharmacySearchController( val hasLocationServiceEnabled = isLocationServiceEnabled(context) val currentLocation = - location ?: if (hasLocationPermission && hasLocationServiceEnabled && filter.nearBy) { + coordinates ?: if (hasLocationPermission && hasLocationServiceEnabled && filter.nearBy) { queryLocation(context) } else { null @@ -212,7 +229,7 @@ class PharmacySearchController( LocationMode.Enabled(it, radiusInMeter) } ?: LocationMode.Disabled - val locationError = if (location == null && filter.nearBy) { + val locationError = if (coordinates == null && filter.nearBy) { when { !hasLocationServiceEnabled -> SearchQueryResult.NoLocationServicesEnabled !hasLocationPermission -> SearchQueryResult.NoLocationPermission @@ -229,7 +246,7 @@ class PharmacySearchController( } else -> { - val isNearBy = locationMode is LocationMode.Enabled && location == null + val isNearBy = locationMode is LocationMode.Enabled && coordinates == null searchChannelFlow.emit( PharmacyUseCaseData.SearchData( name = name, @@ -245,6 +262,7 @@ class PharmacySearchController( } } } + suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { pharmacyOverviewUseCase.deleteOverviewPharmacy(overviewPharmacy) } @@ -255,13 +273,13 @@ class PharmacySearchController( private fun PharmacyUseCaseData.Pharmacy.updateDistanceForEnabledLocation(locationMode: LocationMode) = when (locationMode) { - is LocationMode.Enabled -> copy(distance = location?.minus(locationMode.location)) + is LocationMode.Enabled -> copy(distance = coordinates?.minus(locationMode.coordinates)) else -> this } private fun PharmacyUseCaseData.Pharmacy.providesDeliveryService(isDeliveryServiceFiltered: Boolean) = when { - isDeliveryServiceFiltered -> provides.any { it is DeliveryPharmacyService } + isDeliveryServiceFiltered -> provides.any { it is PharmacyService.DeliveryPharmacyService } else -> true } @@ -284,9 +302,9 @@ private fun isLocationServiceEnabled(context: Context): Boolean { } } -suspend fun queryLocation(context: Context): Location? = +suspend fun queryLocation(context: Context): Coordinates? = queryNativeLocation(context)?.let { - Location(longitude = it.longitude, latitude = it.latitude) + Coordinates(longitude = it.longitude, latitude = it.latitude) } @SuppressLint("MissingPermission") diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchListController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchListController.kt new file mode 100644 index 00000000..da0605b2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchListController.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.cachedIn +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.filter +import androidx.paging.map +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType.Companion.getUpdatedFilter +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import org.kodein.di.compose.rememberInstance + +/** + * Controller for the pharmacy search list + * @param pharmacyFilter filter for the pharmacy filter + * @param coordinates coordinates for the latitude and longitude + */ +class PharmacySearchListController( + pharmacyFilter: PharmacyUseCaseData.Filter, + coordinates: Coordinates?, + searchTerm: String, + private val pharmacySearchUseCase: PharmacySearchUseCase +) : Controller() { + + private val searchTerm = MutableStateFlow(searchTerm) + + private val defaultSearch = PharmacyUseCaseData.SearchData( + name = searchTerm, + filter = pharmacyFilter, + locationMode = if (pharmacyFilter.nearBy) { + coordinates?.let { + PharmacyUseCaseData.LocationMode.Enabled(it) + } ?: PharmacyUseCaseData.LocationMode.Disabled + } else { + PharmacyUseCaseData.LocationMode.Disabled + } + ) + + private val searchParams = MutableStateFlow(defaultSearch) + + fun onSearchTerm(value: String) { + searchTerm.value = value + searchParams.value = searchParams.value.copy(name = value) + } + + fun onFilter( + filterType: FilterType, + block: (FilterType) -> Unit + ) { + searchParams.value = + if (filterType == FilterType.NEARBY && searchParams.value.filter.nearBy) { + searchParams.value.copy( + name = searchTerm.value, + locationMode = PharmacyUseCaseData.LocationMode.Disabled, + filter = filterType.getUpdatedFilter(searchParams.value.filter) + ) + } else { + searchParams.value.copy( + name = searchTerm.value, + filter = filterType.getUpdatedFilter(searchParams.value.filter) + ) + } + block(filterType) + } + + @Requirement( + "A_20285#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "pharmacy state based on search params and filters" + ) + @OptIn(ExperimentalCoroutinesApi::class) + private val pharmacies by lazy { + searchParams.onEach { }.flatMapLatest { searchParams -> + pharmacySearchUseCase.searchPharmacies(searchParams) + .mapNotNull { pagingData -> + pagingData.map { it.location(searchParams.locationMode) } + .filter { it.deliveryService(searchParams.filter.deliveryService) } + .filter { it.isOpenNow(searchParams.filter.openNow) } + }.cachedIn(controllerScope) + } + } + + val pharmaciesState + @Composable + get() = pharmacies.collectAsLazyPagingItems() + + val searchParamState + @Composable + get() = searchParams.collectAsStateWithLifecycle() +} + +@Composable +fun rememberPharmacySearchListController( + filter: PharmacyUseCaseData.Filter = PharmacyUseCaseData.Filter(), + coordinates: Coordinates? = null, + searchTerm: String = WILDCARD +): PharmacySearchListController { + val searchUseCase by rememberInstance() + + return remember(filter, coordinates, searchTerm) { + PharmacySearchListController( + pharmacyFilter = filter, + coordinates = coordinates, + searchTerm = searchTerm, + pharmacySearchUseCase = searchUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchMapsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchMapsController.kt new file mode 100644 index 00000000..ee4bc769 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchMapsController.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn +import org.kodein.di.compose.rememberInstance + +class PharmacySearchMapsController( + pharmacyFilter: PharmacyUseCaseData.Filter, + coordinates: Coordinates?, + private val pharmacyMapsUseCase: PharmacyMapsUseCase +) : Controller() { + + private val defaultSearch by lazy { + PharmacyUseCaseData.MapsSearchData( + name = WILDCARD, + filter = pharmacyFilter, + locationMode = coordinates?.let { + PharmacyUseCaseData.LocationMode.Enabled(it) + } ?: PharmacyUseCaseData.LocationMode.Disabled, + coordinates = coordinates + ) + } + + private val searchParams by lazy { MutableStateFlow(defaultSearch) } + + private val cameraRadius = MutableStateFlow(DefaultRadiusInMeter) + + val areMapsLoadingEvent = ComposableEvent() + + @OptIn(ExperimentalCoroutinesApi::class) + private val pharmacies by lazy { + searchParams.onEach { + searchParams.value = it.copy( + locationMode = (it.locationMode as? PharmacyUseCaseData.LocationMode.Enabled) + ?.copy(radiusInMeter = cameraRadius.value) ?: it.locationMode + ) + }.flatMapLatest { searchParams -> + areMapsLoadingEvent.trigger(true) + flow { + runCatching { + if (searchParams.locationMode is PharmacyUseCaseData.LocationMode.Disabled) { + pharmacyMapsUseCase.invoke(searchParams, cameraRadius.value) + } else { + pharmacyMapsUseCase.invoke(searchParams, cameraRadius.value) + } + .map { it.location(searchParams.locationMode) } + .filter { it.deliveryService(searchParams.filter.deliveryService) } + .filter { it.isOpenNow(searchParams.filter.openNow) } + } + .onSuccess { + areMapsLoadingEvent.trigger(false) + emit(it) + } + .onFailure { + areMapsLoadingEvent.trigger(false) + } + }.shareIn( + scope = controllerScope, + started = kotlinx.coroutines.flow.SharingStarted.Lazily, + replay = 1 + ) + } + } + + fun onCameraRadiusChanged(radius: Double) { + cameraRadius.value = radius + } + + val cameraRadiusState + @Composable + get() = cameraRadius.collectAsStateWithLifecycle() + + val pharmaciesState + @Composable + get() = pharmacies.collectAsStateWithLifecycle(emptyList()) + + val searchParamState + @Composable + get() = searchParams.collectAsStateWithLifecycle() +} + +@Composable +fun rememberPharmacySearchMapsController( + pharmacyFilter: PharmacyUseCaseData.Filter, + coordinates: Coordinates? +): PharmacySearchMapsController { + val mapsUseCase by rememberInstance() + + return remember(pharmacyFilter, coordinates) { + PharmacySearchMapsController( + pharmacyFilter = pharmacyFilter, + coordinates = coordinates, + pharmacyMapsUseCase = mapsUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyStartController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyStartController.kt new file mode 100644 index 00000000..235491f6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyStartController.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.model.SelectedFavouritePharmacyState +import de.gematik.ti.erp.app.pharmacy.usecase.DeleteOverviewPharmacyUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetPharmacyByTelematikIdUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class PharmacyStartController( + private val getPharmacyByTelematikIdUseCase: GetPharmacyByTelematikIdUseCase, + private val deleteOverviewPharmacyUseCase: DeleteOverviewPharmacyUseCase +) : Controller() { + + private val selectedPharmacyByTelematikId = MutableStateFlow(null) + private val _selectedPharmacyState = MutableStateFlow(SelectedFavouritePharmacyState.Idle) + + val selectedPharmacyState: StateFlow = _selectedPharmacyState.asStateFlow() + + fun onPharmacySelected(pharmacy: OverviewPharmacyData.OverviewPharmacy) { + selectedPharmacyByTelematikId.value = pharmacy + getPharmacy() + } + + fun getPharmacy() { + selectedPharmacyByTelematikId.value?.let { + getPharmacyByTelematikId(it.telematikId) + } + } + + fun clearSelectedPharmacy() { + controllerScope.launch { + selectedPharmacyByTelematikId.value?.let { + deleteOverviewPharmacyUseCase(it) + selectedPharmacyByTelematikId.value = null + } + } + } + + private fun getPharmacyByTelematikId(telematikId: String) { + controllerScope.launch { + _selectedPharmacyState.value = SelectedFavouritePharmacyState.Loading + getPharmacyByTelematikIdUseCase(telematikId) + .fold( + onSuccess = { pharmacy -> + if (pharmacy != null) { + _selectedPharmacyState.value = SelectedFavouritePharmacyState.Data(pharmacy) + } else { + _selectedPharmacyState.value = SelectedFavouritePharmacyState.Missing + } + }, + onFailure = { + _selectedPharmacyState.value = SelectedFavouritePharmacyState.Error(it) + } + ) + } + } + + fun resetSelectedPharmacyState() { + _selectedPharmacyState.value = SelectedFavouritePharmacyState.Idle + } +} + +@Composable +fun rememberPharmacyStartController(): PharmacyStartController { + val getPharmacyByTelematikIdUseCase by rememberInstance() + val deleteOverviewPharmacyUseCase by rememberInstance() + return remember { + PharmacyStartController( + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + deleteOverviewPharmacyUseCase = deleteOverviewPharmacyUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt deleted file mode 100644 index 50e64fd0..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator -import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.fhir.model.DirectCommunicationMessage -import de.gematik.ti.erp.app.fhir.model.json -import de.gematik.ti.erp.app.orders.usecase.SaveLocalCommunicationUseCase -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.prescription.repository.RemoteRedeemOption -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions -import de.gematik.ti.erp.app.prescription.presentation.retryWithAuthenticator -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.github.aakira.napier.Napier -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import kotlinx.serialization.encodeToString -import org.kodein.di.compose.rememberInstance -import java.net.HttpURLConnection -import java.util.UUID - -@Stable -class RedeemPrescriptionsController( - private val searchUseCase: PharmacySearchUseCase, - private val pharmacyDirectRedeemUseCase: PharmacyDirectRedeemUseCase, - private val saveLocalCommunicationUseCase: SaveLocalCommunicationUseCase, - private val overviewUseCase: PharmacyOverviewUseCase, - private val dispatchers: DispatchProvider, - private val authenticator: Authenticator -) { - sealed interface State : PrescriptionServiceState { - class Ordered(val orderId: String, val results: Map) : - State - - sealed interface Success : State { - object Ok : Success - } - - sealed interface Error : State, PrescriptionServiceErrorState { - object Unknown : Error - object UnableToRedeem : Error - object IncorrectDataStructure : Error - object JsonViolated : Error - object Timeout : Error - object Conflict : Error - object Gone : Error - object NotFound : Error - } - } - - suspend fun orderPrescriptions( - profileId: ProfileIdentifier, - orderId: UUID, - prescriptions: List, - redeemOption: PharmacyScreenData.OrderOption, - pharmacy: PharmacyUseCaseData.Pharmacy, - contact: PharmacyUseCaseData.ShippingContact - ): PrescriptionServiceState = - orderPrescriptionsFlow( - profileId = profileId, - orderId = orderId, - prescriptions = prescriptions, - redeemOption = redeemOption, - pharmacy = pharmacy, - contact = contact - ).cancellable().first() - - @Requirement( - "A_22778", - "A_22779", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Start Redeem without TI." - ) - suspend fun orderPrescriptionsDirectly( - orderId: UUID, - prescriptions: List, - redeemOption: PharmacyScreenData.OrderOption, - pharmacy: PharmacyUseCaseData.Pharmacy, - contact: PharmacyUseCaseData.ShippingContact - ): PrescriptionServiceState = - orderPrescriptionsDirectlyFlow( - orderId = orderId, - prescriptions = prescriptions, - redeemOption = redeemOption, - pharmacy = pharmacy, - contact = contact - ).cancellable().first() - - @Requirement( - "GS-A_5542#2", - sourceSpecification = "gemSpec_Krypt", - rationale = "Errors from the protocol are handled and delegated." - ) - private fun orderPrescriptionsDirectlyFlow( - orderId: UUID, - prescriptions: List, - redeemOption: PharmacyScreenData.OrderOption, - pharmacy: PharmacyUseCaseData.Pharmacy, - contact: PharmacyUseCaseData.ShippingContact - ) = - flow { - withContext(dispatchers.io) { - val certHolderList = pharmacyDirectRedeemUseCase.loadCertificates( - pharmacy.id - ).getOrNull() - - val transactionId = UUID.randomUUID().toString() - - val results = prescriptions - .map { prescription -> - - val message = DirectCommunicationMessage( - version = 2, - supplyOptionsType = when (redeemOption) { - PharmacyScreenData.OrderOption.PickupService -> RemoteRedeemOption.Local.type - PharmacyScreenData.OrderOption.CourierDelivery -> RemoteRedeemOption.Delivery.type - PharmacyScreenData.OrderOption.MailDelivery -> RemoteRedeemOption.Shipment.type - }, - name = contact.name, - address = listOf(contact.line1, contact.line2, contact.postalCode, contact.city), - phone = contact.telephoneNumber, - hint = contact.deliveryInformation, - text = "", - mail = contact.mail, - transactionID = transactionId, - taskID = prescription.taskId, - accessCode = prescription.accessCode - ) - val messageString = json.encodeToString(message) - - async { - prescription to certHolderList?.let { - pharmacyDirectRedeemUseCase.redeemPrescriptionDirectly( - url = when (redeemOption) { - PharmacyScreenData.OrderOption.CourierDelivery - -> pharmacy.contacts.deliveryUrl - - PharmacyScreenData.OrderOption.PickupService - -> pharmacy.contacts.pickUpUrl - - PharmacyScreenData.OrderOption.MailDelivery - -> pharmacy.contacts.onlineServiceUrl - }, - message = messageString, - telematikId = pharmacy.telematikId, - recipientCertificates = it, - transactionId = transactionId - ) - } - } - } - .awaitAll() - .toMap() - - overviewUseCase.saveOrUpdateUsedPharmacies(pharmacy) - - results.mapValues { (order, result) -> - result?.fold( - onSuccess = { - saveLocalCommunicationUseCase.invoke(order.taskId, pharmacy.id, transactionId) - State.Success.Ok - }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_BAD_REQUEST -> State.Error.IncorrectDataStructure // 400 - HttpURLConnection.HTTP_UNAUTHORIZED -> State.Error.JsonViolated // 401 - HttpURLConnection.HTTP_NOT_FOUND -> State.Error.UnableToRedeem // 404 - HttpURLConnection.HTTP_CLIENT_TIMEOUT -> State.Error.Timeout // 408 - HttpURLConnection.HTTP_CONFLICT -> State.Error.Conflict // 409 - HttpURLConnection.HTTP_GONE -> State.Error.Gone // 410 - HttpURLConnection.HTTP_INTERNAL_ERROR -> State.Error.Unknown // 500 - else -> { - State.Error.Unknown - } - } - } else { - State.Error.Unknown - } - } - ) - } - }.also { - emit(it) - } - }.map { results -> - State.Ordered(orderId.toString(), results) - }.flowOn(dispatchers.io) - - @Requirement( - "GS-A_5542#3", - sourceSpecification = "gemSpec_Krypt", - rationale = "Errors from the protocol are handled and delegated." - ) - private fun orderPrescriptionsFlow( - profileId: ProfileIdentifier, - orderId: UUID, - prescriptions: List, - redeemOption: PharmacyScreenData.OrderOption, - pharmacy: PharmacyUseCaseData.Pharmacy, - contact: PharmacyUseCaseData.ShippingContact - ) = - flow { - withContext(dispatchers.io) { - val results = prescriptions - .map { prescription -> - async { - prescription to searchUseCase.redeemPrescription( - orderId = orderId, - profileId = profileId, - redeemOption = when (redeemOption) { - PharmacyScreenData.OrderOption.PickupService -> RemoteRedeemOption.Local - PharmacyScreenData.OrderOption.CourierDelivery -> RemoteRedeemOption.Delivery - PharmacyScreenData.OrderOption.MailDelivery -> RemoteRedeemOption.Shipment - }, - order = prescription, - contact = contact, - pharmacyTelematikId = pharmacy.telematikId - ) - } - } - .awaitAll() - .map { - Napier.d { "orders are $it" } - it - } - .toMap() - - overviewUseCase.saveOrUpdateUsedPharmacies(pharmacy) - - results.mapValues { (_, result) -> - result.fold( - onSuccess = { - State.Success.Ok - }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_BAD_REQUEST -> State.Error.IncorrectDataStructure // 400 - HttpURLConnection.HTTP_UNAUTHORIZED -> State.Error.JsonViolated // 401 - HttpURLConnection.HTTP_CLIENT_TIMEOUT -> State.Error.Timeout // 408 - HttpURLConnection.HTTP_CONFLICT -> State.Error.Conflict // 409 - HttpURLConnection.HTTP_GONE -> State.Error.Gone // 410 - HttpURLConnection.HTTP_INTERNAL_ERROR -> State.Error.Unknown // 500 - else -> { - throw it - } - } - } else { - throw it - } - } - ) - } - }.also { - emit(it) - } - }.map { results -> - Napier.d { "State.Ordered(orderId.toString(), results) ${State.Ordered(orderId.toString(), results)}" } - State.Ordered(orderId.toString(), results) - } - .retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .catch { - // TODO: remove for better error handling - emit(State.Error.Unknown) - } - .flowOn(dispatchers.io) -} - -@Composable -fun rememberRedeemPrescriptionsController(): RedeemPrescriptionsController { - val searchUseCase by rememberInstance() - val pharmacyDirectRedeemUseCase by rememberInstance() - val saveLocalCommunicationUseCase by rememberInstance() - val overviewUseCase by rememberInstance() - val dispatchers by rememberInstance() - val authenticator = LocalAuthenticator.current - return remember { - RedeemPrescriptionsController( - searchUseCase = searchUseCase, - pharmacyDirectRedeemUseCase = pharmacyDirectRedeemUseCase, - saveLocalCommunicationUseCase = saveLocalCommunicationUseCase, - overviewUseCase = overviewUseCase, - dispatchers = dispatchers, - authenticator = authenticator - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PreviewMapCoordinatesRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PreviewMapCoordinatesRepository.kt new file mode 100644 index 00000000..7e95b661 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PreviewMapCoordinatesRepository.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository + +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PreviewMapCoordinatesDataSource + +class PreviewMapCoordinatesRepository( + private val dataSource: PreviewMapCoordinatesDataSource +) { + fun setPreviewCoordinates( + coordinates: Coordinates? + ) { + coordinates?.let { + dataSource.coordinates.value = it + } ?: run { + dataSource.coordinates.value = PreviewMapCoordinatesDataSource.berlinCoordinates + } + } + + fun getPreviewCoordinates() = dataSource.coordinates +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PreviewMapCoordinatesDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PreviewMapCoordinatesDataSource.kt new file mode 100644 index 00000000..12a85ba1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PreviewMapCoordinatesDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository.datasource + +import de.gematik.ti.erp.app.fhir.model.Coordinates +import kotlinx.coroutines.flow.MutableStateFlow + +class PreviewMapCoordinatesDataSource { + val coordinates = MutableStateFlow(berlinCoordinates) + companion object { + val berlinCoordinates = Coordinates(52.51947562977698, 13.404335795642881) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt deleted file mode 100644 index 35526d94..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.MutatorMutex -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.isImeVisible -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.max -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.ui.model.addressSupplementInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.cityInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.deliveryInformationInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.mailInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.nameInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.phoneNumberInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.postalCodeInputField -import de.gematik.ti.erp.app.pharmacy.ui.model.streetAndNumberInputField -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyCity -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyLine1 -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyMail -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyName -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyPhoneNumber -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyPostalCode -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidCity -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidDeliveryInformation -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidLine1 -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidLine2 -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidMail -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidName -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidPhoneNumber -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidPostalCode -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isValid -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.BottomAppBar -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import io.github.aakira.napier.Napier -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -data class ValidationResult( - val isEmpty: Boolean, - val isInvalid: Boolean -) - -@Requirement( - "O.Purp_2#6", - "O.Data_6#6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Contact information is collected when needed for redeeming." -) -@Suppress("LongMethod") -@Composable -fun EditShippingContactScreen( - pharmacyOrderController: PharmacyOrderController, - onBack: () -> Unit -) { - val listState = rememberLazyListState() - - val orderState by pharmacyOrderController.orderState - val selectedOrderOption = remember { pharmacyOrderController.selectedOrderOption } - - var contact by remember(orderState.contact) { mutableStateOf(orderState.contact) } - val shippingContactState = remember(orderState, contact) { - if (selectedOrderOption != null) { - pharmacyOrderController.shippingContactState(contact, selectedOrderOption) - } else null - } - - val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState - - var showBackAlert by remember { mutableStateOf(false) } - - if (showBackAlert) { BackAlert(onCancel = { showBackAlert = false }, onBack = onBack) } - - shippingContactState?.let { state -> - - AnimatedElevationScaffold( - navigationMode = NavigationBarMode.Back, - bottomBar = { - ContactBottomBar( - enabled = state.isValid(), - onClick = { - pharmacyOrderController.onSaveContact(contact) - onBack() - } - ) - }, - topBarTitle = stringResource(R.string.edit_shipping_contact_top_bar_title), - listState = listState, - onBack = { - if (state.isValid()) { - pharmacyOrderController.onSaveContact(contact) - onBack() - } else { - showBackAlert = true - } - } - ) { contentPadding -> - val imePadding = WindowInsets.ime.asPaddingValues() - - val focusManager = LocalFocusManager.current - - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = listState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - contentPadding = PaddingValues( - top = PaddingDefaults.Medium + contentPadding.calculateTopPadding(), - bottom = PaddingDefaults.Medium + max( - imePadding.calculateBottomPadding(), - contentPadding.calculateBottomPadding() - ), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - ) { - item { ContactHeader() } - phoneNumberInputField( - listState = listState, - value = contact.telephoneNumber, - validationResult = ValidationResult( - isEmpty = state.isEmptyPhoneNumber(), - isInvalid = state.isInvalidPhoneNumber() - ), - onValueChange = { phone -> - contact = contact.copy( - telephoneNumber = phone.trim() - ) - }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) } - ) - // we sent the mail currently only on direct redeem - if (directRedeemEnabled) { - mailInputField( - listState = listState, - validationResult = ValidationResult( - isEmpty = state.isEmptyMail(), - isInvalid = state.isInvalidMail() - ), - value = contact.mail, - onValueChange = { mail -> contact = (contact.copy(mail = mail.trim())) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) } - ) - } - - item { AddressHeader() } - - nameInputField( - listState = listState, - value = contact.name, - onValueChange = { name -> contact = (contact.copy(name = name)) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - validationResult = ValidationResult( - isEmpty = state.isEmptyName(), - isInvalid = state.isInvalidName() - ) - ) - - streetAndNumberInputField( - listState = listState, - value = contact.line1, - onValueChange = { line1 -> contact = (contact.copy(line1 = line1)) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - validationResult = ValidationResult( - isEmpty = state.isEmptyLine1(), - isInvalid = state.isInvalidLine1() - ) - ) - - addressSupplementInputField( - listState = listState, - value = contact.line2, - validationResult = ValidationResult( - isEmpty = false, // optional, - isInvalid = state.isInvalidLine2() - ), - onValueChange = { line2 -> contact = (contact.copy(line2 = line2)) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) } - ) - - postalCodeInputField( - listState = listState, - value = contact.postalCode, - onValueChange = { postalCode -> - contact = (contact.copy(postalCode = postalCode)) - }, - validationResult = ValidationResult( - isEmpty = state.isEmptyPostalCode(), - isInvalid = state.isInvalidPostalCode() - ), - onSubmit = { focusManager.moveFocus(FocusDirection.Down) } - ) - - cityInputField( - listState = listState, - value = contact.city, - onValueChange = { city -> - contact = (contact.copy(city = city)) - }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - validationResult = ValidationResult( - isEmpty = state.isEmptyCity(), - isInvalid = state.isInvalidCity() - ) - ) - - deliveryInformationInputField( - listState = listState, - value = contact.deliveryInformation, - validationResult = ValidationResult( - isEmpty = false, // optional, - isInvalid = state.isInvalidDeliveryInformation() - ), - onValueChange = { deliveryInformation -> - contact = (contact.copy(deliveryInformation = deliveryInformation)) - }, - onSubmit = { focusManager.clearFocus() } - ) - } - } ?: run { - Napier.e { "ShippingContact is null" } - } - } -} - -@Composable -fun ContactBottomBar(enabled: Boolean, onClick: () -> Unit) { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(Modifier.weight(1f)) - Button( - onClick = onClick, - enabled = enabled - ) { - Text(stringResource(R.string.edit_shipping_contact_save)) - } - SpacerSmall() - } -} - -@Composable -fun AddressHeader() { - SpacerLarge() - Text( - stringResource(R.string.edit_shipping_contact_title_address), - style = AppTheme.typography.h6 - ) -} - -@Composable -fun BackAlert(onCancel: () -> Unit, onBack: () -> Unit) { - CommonAlertDialog( - header = stringResource(R.string.edit_contact_back_alert_header), - info = stringResource(R.string.edit_contact_back_alert_information), - onCancel = onCancel, - onClickAction = onBack, - cancelText = stringResource(R.string.edit_contact_back_alert_change), - actionText = stringResource(R.string.edit_contact_back_alert_action) - ) -} - -@Composable -fun ContactHeader() { - Text( - stringResource(R.string.edit_shipping_contact_title_contact), - style = AppTheme.typography.h6 - ) -} - -private const val LayoutDelay = 330L - -// TODO: Move to a different place, used in many places -@OptIn(ExperimentalLayoutApi::class) -fun Modifier.scrollOnFocus(to: Int, listState: LazyListState, offset: Int = 0) = composed { - val coroutineScope = rememberCoroutineScope() - val mutex = MutatorMutex() - - var hasFocus by remember { mutableStateOf(false) } - val keyboardVisible = WindowInsets.isImeVisible - - LaunchedEffect(hasFocus, keyboardVisible) { - if (hasFocus && keyboardVisible) { - mutex.mutate { - delay(LayoutDelay) - listState.animateScrollToItem(to, offset) - } - } - } - - onFocusChanged { - if (it.hasFocus) { - hasFocus = true - coroutineScope.launch { - mutex.mutate(MutatePriority.UserInput) { - delay(LayoutDelay) - listState.animateScrollToItem(to, offset) - } - } - } else { - hasFocus = false - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt deleted file mode 100644 index 5f37cc3c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Star -import androidx.compose.material.icons.rounded.StarBorder -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.TertiaryButton -import de.gematik.ti.erp.app.utils.compose.shortToast - -@Composable -internal fun FavoriteStarButton( - isMarked: Boolean, - modifier: Modifier = Modifier, - onChange: (Boolean) -> Unit -) { - val color = if (isMarked) { - AppTheme.colors.yellow500 - } else { - AppTheme.colors.primary600 - } - - val icon = if (isMarked) { - Icons.Rounded.Star - } else { - Icons.Rounded.StarBorder - } - - val addedText = stringResource(R.string.pharmacy_detals_added_to_favorites) - val removedText = stringResource(R.string.pharmacy_detalls_removed_from_favorites) - - val context = LocalContext.current - val haptic = LocalHapticFeedback.current - - TertiaryButton( - modifier = modifier.size(56.dp), - onClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - onChange(!isMarked) - context.shortToast( - when { - !isMarked -> addedText - else -> removedText - } - ) - }, - contentPadding = PaddingValues(PaddingDefaults.Medium) - ) { - Icon( - icon, - contentDescription = null, - tint = color - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt deleted file mode 100644 index 76f99961..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Star -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -@Stable -private sealed interface RefreshState { - @Stable - object Loading : RefreshState - - @Stable - class Success(val pharmacy: PharmacyUseCaseData.Pharmacy) : RefreshState - - @Stable - object NotFound : RefreshState - - @Stable - object Error : RefreshState -} - -@Composable -fun FavoritePharmacyCard( - modifier: Modifier = Modifier, - overviewPharmacy: OverviewPharmacyData.OverviewPharmacy, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, - pharmacySearchController: PharmacySearchController -) { - var showFailedPharmacyCallDialog by remember { mutableStateOf(false) } - var showNoInternetConnectionDialog by remember { mutableStateOf(false) } - - var state by remember { mutableStateOf(RefreshState.Loading) } - LaunchedEffect(overviewPharmacy) { - refresh( - pharmacySearchController = pharmacySearchController, - pharmacyTelematikId = overviewPharmacy.telematikId, - onStateChange = { - state = it - } - ) - } - - val scope = rememberCoroutineScope() - - if (showNoInternetConnectionDialog) { - CommonAlertDialog( - header = stringResource(R.string.pharmacy_search_apovz_call_no_internet_header), - info = stringResource(R.string.pharmacy_search_apovz_call_no_internet_info), - cancelText = stringResource(R.string.pharmacy_search_apovz_call_no_internet_cancel), - actionText = stringResource(R.string.pharmacy_search_apovz_call_no_internet_retry), - onCancel = { showNoInternetConnectionDialog = false }, - onClickAction = { - scope.launch { - refresh( - pharmacySearchController = pharmacySearchController, - pharmacyTelematikId = overviewPharmacy.telematikId, - onStateChange = { - state = it - } - ) - } - } - ) - } - if (showFailedPharmacyCallDialog) { - AcceptDialog( - header = stringResource(R.string.pharmacy_search_apovz_call_failed_header), - info = stringResource(R.string.pharmacy_search_apovz_call_failed_body), - onClickAccept = { - scope.launch { pharmacySearchController.deleteOverviewPharmacy(overviewPharmacy) } - showFailedPharmacyCallDialog = false - }, - acceptText = stringResource(R.string.pharmacy_search_apovz_call_failed_accept) - ) - } - - Card( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .then(modifier) - .clickable(role = Role.Button) { - when (state) { - is RefreshState.Success -> onSelectPharmacy((state as RefreshState.Success).pharmacy) - is RefreshState.Error -> showNoInternetConnectionDialog = true - is RefreshState.NotFound -> showFailedPharmacyCallDialog = true - else -> {} - } - }, - shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - elevation = 0.dp - ) { - CardContent(overviewPharmacy) - } -} - -private suspend fun refresh( - pharmacySearchController: PharmacySearchController, - pharmacyTelematikId: String, - onStateChange: (RefreshState) -> Unit -) { - onStateChange(RefreshState.Loading) - val result = pharmacySearchController.findPharmacyByTelematikIdState(pharmacyTelematikId).first().fold( - onFailure = { - Napier.e("Could not find pharmacy by telematikId", it) - RefreshState.Error - }, - onSuccess = { - if (it == null) { - RefreshState.NotFound - } else { - RefreshState.Success(it) - } - } - ) - onStateChange(result) -} - -@Composable -private fun CardContent(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround - ) { - PharmacyImagePlaceholder(Modifier.padding(PaddingDefaults.Medium)) - - Column( - modifier = Modifier - .padding( - end = PaddingDefaults.Medium, - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium - ) - .weight(1f) - ) { - Text( - overviewPharmacy.pharmacyName, - style = AppTheme.typography.subtitle1 - ) - Text( - overviewPharmacy.address, - style = AppTheme.typography.body2, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - if (overviewPharmacy.isFavorite) { - Icon( - Icons.Rounded.Star, - contentDescription = null, - modifier = Modifier - .padding(end = PaddingDefaults.Medium) - .size(24.dp), - tint = AppTheme.colors.yellow500 - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt deleted file mode 100644 index 1aac25f3..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt +++ /dev/null @@ -1,762 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.content.Context -import android.graphics.Point -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.material.SnackbarHost -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Tune -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.MyLocation -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.LocationSource -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import com.google.android.gms.maps.model.CameraPosition -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds -import com.google.maps.android.SphericalUtil -import com.google.maps.android.compose.CameraPositionState -import com.google.maps.android.compose.GoogleMap -import com.google.maps.android.compose.MapProperties -import com.google.maps.android.compose.MapUiSettings -import com.google.maps.android.compose.Marker -import com.google.maps.android.compose.rememberCameraPositionState -import com.google.maps.android.compose.rememberMarkerState -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.PopUpName -import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.core.complexAutoSaver -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.fhir.model.Location -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController -import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions -import de.gematik.ti.erp.app.pharmacy.presentation.queryNativeLocation -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacySearchPopUpNames -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -private val Berlin = LatLng(52.51947562977698, 13.404335795642881) -private const val DefaultZoomLevel = 12.2f -private const val MyLocationZoomLevel = 15f - -private fun Location.toLatLng() = - LatLng(latitude, longitude) - -private fun PharmacyUseCaseData.LocationMode.Enabled.toLatLng() = - location.toLatLng() - -@Composable -fun MapsOverviewSmall( - modifier: Modifier, - onClick: () -> Unit -) { - val cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(Berlin, DefaultZoomLevel) - } - Box(modifier) { - GoogleMap( - modifier = Modifier - .clip(RoundedCornerShape(16.dp)), - cameraPositionState = cameraPositionState, - uiSettings = remember { - MapUiSettings( - compassEnabled = false, - indoorLevelPickerEnabled = false, - mapToolbarEnabled = false, - myLocationButtonEnabled = false, - rotationGesturesEnabled = false, - scrollGesturesEnabled = false, - scrollGesturesEnabledDuringRotateOrZoom = false, - tiltGesturesEnabled = false, - zoomControlsEnabled = false, - zoomGesturesEnabled = false - ) - } - ) - Box( - Modifier - .fillMaxSize() - .clip(RoundedCornerShape(16.dp)) - .clickable(onClick = onClick) - ) - } -} - -@Stable -sealed interface PharmacySearchSheetContentState { - - @Stable - data class PharmacySelected( - val pharmacy: PharmacyUseCaseData.Pharmacy, - val popUp: PopUpName = PharmacySearchPopUpNames.PharmacySelected - ) : PharmacySearchSheetContentState - - @Stable - data class FilterSelected( - val popUp: PopUpName = PharmacySearchPopUpNames.FilterSelected - ) : PharmacySearchSheetContentState -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun MapsOverview( - searchController: PharmacySearchController, - pharmacyOrderController: PharmacyOrderController, - navController: NavHostController, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, - onBack: () -> Unit -) { - val context = LocalContext.current - val scaffoldState = rememberScaffoldState() - - val cameraPositionState = rememberCameraPositionState { - val latLng = - (searchController.searchState.locationMode as? PharmacyUseCaseData.LocationMode.Enabled)?.toLatLng() - ?: Berlin - position = CameraPosition.fromLatLngZoom(latLng, DefaultZoomLevel) - } - - var pharmacies by rememberSaveable(saver = complexAutoSaver()) { - mutableStateOf>( - emptyList() - ) - } - LaunchedEffect(Unit) { - searchController - .pharmacyMapsFlow - .collect { result -> - when (result) { - is PharmacySearchController.State.Pharmacies -> - pharmacies = result.pharmacies - - is GeneralErrorState -> - mapsErrorMessage(context, result)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - } - - var showSearchButton by rememberSaveable { mutableStateOf(false) } - CameraAnimation( - cameraPositionState = cameraPositionState, - pharmacySearchController = searchController, - pharmacies = pharmacies, - onShowSearchButton = { - showSearchButton = true - } - ) - - val scope = rememberCoroutineScope() - - val sheetState = rememberPharmacySheetState( - pharmacyOrderController.selectedPharmacy?.let { - PharmacySearchSheetContentState.PharmacySelected(it) - } - ) - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - analytics.trackPharmacySearchPopUps(sheetState.content) - } else { - analytics.onPopUpClosed() - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - Box { - ScaffoldWithMap( - scaffoldState = scaffoldState, - pharmacyOrderController = pharmacyOrderController, - cameraPositionState = cameraPositionState, - pharmacySearchController = searchController, - pharmacies = pharmacies, - showSearchButton = showSearchButton, - onShowSearchButton = { - showSearchButton = it - }, - onShowBottomSheet = { - sheetState.show(it) - }, - onBack = onBack - ) - - ModalBottomSheet( - sheetState = sheetState, - sheetContent = { - when (sheetState.content) { - is PharmacySearchSheetContentState.FilterSelected -> - FilterSheetContent( - modifier = Modifier.navigationBarsPadding(), - filter = searchController.searchState.filter, - onClickChip = { filter -> - scope.launch { - val l = cameraPositionState.position.target - val radius = SphericalUtil.computeDistanceBetween( - cameraPositionState.projection?.visibleRegion?.latLngBounds?.northeast, - cameraPositionState.projection?.visibleRegion?.latLngBounds?.southwest - ) / 2.0 - searchController.search( - searchController.searchState.name, - filter.copy(nearBy = false), - Location(latitude = l.latitude, longitude = l.longitude), - radius - ) - } - }, - onClickClose = { scope.launch { sheetState.animateTo(ModalBottomSheetValue.Hidden) } }, - showNearByFilter = false - ) - - is PharmacySearchSheetContentState.PharmacySelected -> - PharmacyBottomSheetDetails( - orderController = pharmacyOrderController, - pharmacy = - (sheetState.content as PharmacySearchSheetContentState.PharmacySelected).pharmacy, - onClickOrder = { pharmacy, orderOption -> - onSelectPharmacy(pharmacy, orderOption) - } - ) - } - }, - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) } - ) - } -} - -@Composable -fun rememberPharmacySheetState( - content: PharmacySearchSheetContentState? = null -): PharmacySheetState { - val scope = rememberCoroutineScope() - val state = rememberSaveable(saver = complexAutoSaver(init = { this.scope = scope })) { - PharmacySheetState(content ?: PharmacySearchSheetContentState.FilterSelected()) - .apply { this.scope = scope } - } - LaunchedEffect(content) { - content?.let { state.show(content, snap = true) } - } - return state -} - -@Composable -private fun ScaffoldWithMap( - scaffoldState: ScaffoldState, - pharmacyOrderController: PharmacyOrderController, - cameraPositionState: CameraPositionState, - pharmacySearchController: PharmacySearchController, - pharmacies: List, - showSearchButton: Boolean, - onShowSearchButton: (Boolean) -> Unit, - onShowBottomSheet: (PharmacySearchSheetContentState) -> Unit, - onBack: () -> Unit -) { - val scope = rememberCoroutineScope() - - var showNoLocationDialog by remember { mutableStateOf(false) } - - @Requirement( - "A_20193#1", - "A_21154", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Request user permission for location services." - ) - val locationPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - if (permissions.values.any { it }) { - scope.launch { - val radius = SphericalUtil.computeDistanceBetween( - cameraPositionState.projection?.visibleRegion?.latLngBounds?.northeast, - cameraPositionState.projection?.visibleRegion?.latLngBounds?.southwest - ) / 2.0 - - pharmacySearchController.search( - pharmacySearchController.searchState.name, - pharmacySearchController.searchState.filter.copy(nearBy = true), - radiusInMeter = radius - ) - } - } else { - showNoLocationDialog = true - } - } - - if (showNoLocationDialog) { - NoLocationDialog( - onAccept = { - showNoLocationDialog = false - } - ) - } - - var showNoLocationServicesDialog by remember { mutableStateOf(false) } - if (showNoLocationServicesDialog) { - NoLocationServicesDialog( - onClose = { - showNoLocationServicesDialog = false - } - ) - } - - Scaffold( - scaffoldState = scaffoldState, - modifier = Modifier.fillMaxSize(), - snackbarHost = { - SnackbarHost(it, modifier = Modifier.systemBarsPadding()) - } - ) { innerPadding -> - Box { - FullscreenMap( - orderState = pharmacyOrderController, - cameraPositionState = cameraPositionState, - innerPadding = innerPadding, - pharmacies = pharmacies, - onClickMarker = { - onShowBottomSheet(PharmacySearchSheetContentState.PharmacySelected(it)) - } - ) - - MapOverlay( - showSearchButton = showSearchButton, - isLoading = pharmacySearchController.isLoading, - onSearch = { - if (!pharmacySearchController.isLoading) { - scope.launch { - onShowSearchButton(false) - - val radius = SphericalUtil.computeDistanceBetween( - cameraPositionState.projection?.visibleRegion?.latLngBounds?.northeast, - cameraPositionState.projection?.visibleRegion?.latLngBounds?.southwest - ) / 2.0 - - if (it) { - // search here button - val l = cameraPositionState.position.target - pharmacySearchController.search( - pharmacySearchController.searchState.name, - pharmacySearchController.searchState.filter.copy(nearBy = false), - Location(latitude = l.latitude, longitude = l.longitude), - radius - ) - } else { - // find me button - pharmacySearchController.search( - pharmacySearchController.searchState.name, - pharmacySearchController.searchState.filter.copy(nearBy = true), - radiusInMeter = radius - ) - }.also { - when (it) { - PharmacySearchController.SearchQueryResult.NoLocationPermission -> { - locationPermissionLauncher.launch(locationPermissions) - } - - PharmacySearchController.SearchQueryResult.NoLocationFound -> { - showNoLocationDialog = true - onShowSearchButton(true) - } - - PharmacySearchController.SearchQueryResult.NoLocationServicesEnabled -> { - showNoLocationServicesDialog = true - onShowSearchButton(true) - } - - else -> { - } - } - } - } - } - }, - onClickFilter = { - onShowBottomSheet(PharmacySearchSheetContentState.FilterSelected()) - }, - onBack = onBack - ) - } - } -} - -@Composable -private fun CameraAnimation( - cameraPositionState: CameraPositionState, - pharmacySearchController: PharmacySearchController, - pharmacies: List, - onShowSearchButton: () -> Unit -) { - var lastMarkerCenter by remember { mutableStateOf(Berlin) } - val isMoving by remember { - derivedStateOf { cameraPositionState.isMoving } - } - - val moveDistance = with(LocalDensity.current) { 24.dp.roundToPx() } - LaunchedEffect(isMoving) { - if (!isMoving) { - cameraPositionState.projection?.let { projection -> - val latLng = - ( - pharmacySearchController.searchState.locationMode as? - PharmacyUseCaseData.LocationMode.Enabled - )?.toLatLng() - ?: lastMarkerCenter - val d = SphericalUtil.computeDistanceBetween(cameraPositionState.position.target, latLng) - val a = projection.fromScreenLocation(Point(0, 0)) - val b = projection.fromScreenLocation(Point(moveDistance, 0)) - if (d >= SphericalUtil.computeDistanceBetween(a, b)) { - onShowSearchButton() - } - } - } - } - - val padding = with(LocalDensity.current) { PaddingDefaults.XXLarge.roundToPx() } - LaunchedEffect(pharmacies) { - if (pharmacySearchController.searchState.filter.nearBy) { - val latLng = - (pharmacySearchController.searchState.locationMode as? PharmacyUseCaseData.LocationMode.Enabled) - ?.toLatLng() - ?: lastMarkerCenter - - cameraPositionState.animate( - CameraUpdateFactory.newLatLngZoom(latLng, MyLocationZoomLevel), - durationMs = 730 - ) - } else if (pharmacies.isNotEmpty()) { - val bounds = LatLngBounds.builder().apply { - pharmacies.forEach { - include(it.location!!.toLatLng()) - } - }.build() - cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding), durationMs = 730) - lastMarkerCenter = bounds.center - } - } -} - -@Composable -private fun FullscreenMap( - orderState: PharmacyOrderController, - cameraPositionState: CameraPositionState, - innerPadding: PaddingValues, - pharmacies: List, - onClickMarker: (PharmacyUseCaseData.Pharmacy) -> Unit -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - - var selectedPharmacy by remember(orderState.selectedPharmacy) { - mutableStateOf(orderState.selectedPharmacy) - } - - GoogleMap( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - locationSource = remember { locationSourceOnce(context, scope) }, - cameraPositionState = cameraPositionState, - uiSettings = remember { - MapUiSettings( - compassEnabled = false, - indoorLevelPickerEnabled = false, - mapToolbarEnabled = false, - myLocationButtonEnabled = false, - rotationGesturesEnabled = false, - scrollGesturesEnabled = true, - scrollGesturesEnabledDuringRotateOrZoom = true, - tiltGesturesEnabled = false, - zoomControlsEnabled = false, - zoomGesturesEnabled = true - ) - }, - properties = remember { - MapProperties( - isMyLocationEnabled = true - ) - }, - contentPadding = WindowInsets.Companion.systemBars.asPaddingValues(), - content = { - key(pharmacies) { - MapsContent( - pharmacyMapsResult = pharmacies, - onClick = { - selectedPharmacy = it - onClickMarker(it) - } - ) - } - key(selectedPharmacy) { - val markerIcon = remember { BitmapDescriptorFactory.fromResource(R.drawable.maps_marker_red) } - selectedPharmacy?.let { pharmacy -> - pharmacy.location?.let { location -> - val latLng = LatLng(location.latitude, location.longitude) - Marker( - state = rememberMarkerState( - position = latLng - ), - onClick = { - selectedPharmacy = pharmacy - onClickMarker(pharmacy) - false - }, - icon = markerIcon, - zIndex = 9999f - ) - } - } - } - } - ) -} - -@Composable -private fun MapsContent( - pharmacyMapsResult: List, - onClick: (PharmacyUseCaseData.Pharmacy) -> Unit -) { - val markerIcon = remember { BitmapDescriptorFactory.fromResource(R.drawable.maps_marker) } - pharmacyMapsResult.mapNotNull { pharmacy -> - pharmacy.location?.let { location -> - val latLng = LatLng(location.latitude, location.longitude) - Marker( - state = rememberMarkerState( - position = latLng - ), - icon = markerIcon, - onClick = { - onClick(pharmacy) - false - } - ) - } - } -} - -@Composable -private fun BoxScope.MapOverlay( - showSearchButton: Boolean, - isLoading: Boolean, - onSearch: (Boolean) -> Unit, - onClickFilter: () -> Unit, - onBack: () -> Unit -) { - Row( - Modifier - .fillMaxWidth() - .padding(start = PaddingDefaults.Small, end = PaddingDefaults.Medium) - .systemBarsPadding(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - IconButton( - onClick = onBack - ) { - Box( - Modifier - .size(32.dp) - .shadow(2.dp, CircleShape) - .background(AppTheme.colors.neutral100, CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Rounded.Close, - contentDescription = null, - tint = AppTheme.colors.primary600, - modifier = Modifier.size(24.dp) - ) - } - } - - IconButton( - modifier = Modifier - .size(48.dp) - .shadow(2.dp, CircleShape) - .border(1.dp, AppTheme.colors.neutral300, CircleShape) - .background(AppTheme.colors.neutral100, CircleShape), - onClick = onClickFilter - ) { - Icon( - Icons.Outlined.Tune, - contentDescription = null, - tint = AppTheme.colors.primary600, - modifier = Modifier.size(24.dp) - ) - } - } - - Column( - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .systemBarsPadding() - ) { - IconButton( - modifier = Modifier - .align(Alignment.End) - .padding(horizontal = PaddingDefaults.Medium) - .padding(bottom = 80.dp) - .size(56.dp) - .shadow(2.dp, CircleShape) - .border(1.dp, AppTheme.colors.neutral300, CircleShape) - .background(AppTheme.colors.neutral100, CircleShape), - onClick = { - onSearch(false) - } - ) { - Crossfade( - targetState = isLoading - ) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - if (it) { - CircularProgressIndicator( - strokeWidth = 3.dp - ) - } else { - Icon( - Icons.Rounded.MyLocation, - contentDescription = null, - tint = AppTheme.colors.primary600, - modifier = Modifier.size(24.dp) - ) - } - } - } - } - - AnimatedVisibility( - modifier = Modifier - .align(Alignment.CenterHorizontally), - visible = showSearchButton, - enter = fadeIn() + expandVertically(expandFrom = Alignment.Top) + slideInVertically(), - exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top) + slideOutVertically() - ) { - PrimaryButtonSmall( - onClick = { - onSearch(true) - }, - modifier = Modifier - .padding(bottom = PaddingDefaults.Large) - ) { - Icon(Icons.Rounded.Search, null) - SpacerSmall() - Text(stringResource(R.string.pharmacy_maps_search_here_button)) - } - } - } -} - -fun locationSourceOnce(context: Context, coroutineScope: CoroutineScope) = object : LocationSource { - private var currentListener = MutableStateFlow(null) - - init { - coroutineScope.launch { - currentListener.collectLatest { listener -> - if (listener != null) { - queryNativeLocation(context)?.let { - listener.onLocationChanged(it) - } - } - } - } - } - - override fun activate(listener: LocationSource.OnLocationChangedListener) { - currentListener.value = listener - } - - override fun deactivate() { - currentListener.value = null - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt deleted file mode 100644 index 7d9f7e03..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.content.Context -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState - -fun mapsErrorMessage(context: Context, deleteState: PrescriptionServiceErrorState): String? = - when (deleteState) { - GeneralErrorState.NetworkNotAvailable -> - context.getString(R.string.error_message_network_not_available) - else -> null - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt deleted file mode 100644 index 2dbe8c0c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController -import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions -import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchController -import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyNavigationScreens -import de.gematik.ti.erp.app.utils.compose.NavigationAnimation -import de.gematik.ti.erp.app.utils.compose.NavigationMode -import de.gematik.ti.erp.app.utils.compose.navigationModeState -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -@Suppress("LongMethod") -@Composable -fun PharmacyNavigation( - pharmacyOrderController: PharmacyOrderController = rememberPharmacyOrderController(), - isNestedNavigation: Boolean = false, - onBack: () -> Unit, - onFinish: () -> Unit -) { - val scope = rememberCoroutineScope() - val pharmacySearchController = rememberPharmacySearchController() - val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState - var searchFilter by remember(pharmacySearchController.searchState.filter) { - mutableStateOf(pharmacySearchController.searchState.filter) - } - val hasRedeemableOrders by pharmacyOrderController.hasRedeemableOrdersState - - LaunchedEffect(Unit) { - searchFilter = searchFilter.copy(directRedeem = false) - if (directRedeemEnabled && hasRedeemableOrders) { - searchFilter = searchFilter.copy(directRedeem = true) - } - } - - var showNoLocationDialog by remember { mutableStateOf(false) } - - val searchAgainFn = { nearBy: Boolean -> - scope.launch { - pharmacySearchController.search( - name = "", - filter = searchFilter.copy(nearBy = nearBy) - ) - } - } - - val locationPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - if (permissions.values.any { it }) { - searchAgainFn(true) - } else { - showNoLocationDialog = true - } - } - - if (showNoLocationDialog) { - NoLocationDialog( - onAccept = { - searchAgainFn(false) - showNoLocationDialog = false - } - ) - } - - var showNoLocationServicesDialog by remember { mutableStateOf(false) } - if (showNoLocationServicesDialog) { - NoLocationServicesDialog( - onClose = { - searchAgainFn(false) - showNoLocationServicesDialog = false - } - ) - } - - val navController = rememberNavController() - val navigationMode by navController - .navigationModeState(PharmacyNavigationScreens.StartSearch.route) { prev, curr -> - when { - isNestedNavigation && prev == null -> - NavigationMode.Forward - - prev == PharmacyNavigationScreens.StartSearch.route && - (curr == PharmacyNavigationScreens.Maps.route || curr == PharmacyNavigationScreens.List.route) -> - NavigationMode.Open - - (prev == PharmacyNavigationScreens.Maps.route || prev == PharmacyNavigationScreens.List.route) && - curr == PharmacyNavigationScreens.StartSearch.route -> - NavigationMode.Closed - - else -> null - } - } - - var previousNavEntry by remember { mutableStateOf("pharmacySearch") } - trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - - val handleSearchResultFn = { searchResult: PharmacySearchController.SearchQueryResult -> - when (searchResult) { - PharmacySearchController.SearchQueryResult.NoLocationPermission -> { - locationPermissionLauncher.launch(locationPermissions) - } - - PharmacySearchController.SearchQueryResult.NoLocationServicesEnabled -> { - showNoLocationServicesDialog = true - } - - else -> {} - } - } - - NavHost( - navController, - startDestination = PharmacyNavigationScreens.StartSearch.route - ) { - composable(PharmacyNavigationScreens.StartSearch.route) { - NavigationAnimation(mode = navigationMode) { - PharmacyOverviewScreen( - isNestedNavigation = isNestedNavigation, - pharmacyOrderController = pharmacyOrderController, - onBack = onBack, - navController = navController, - onFilterChange = { searchFilter = it }, - filter = searchFilter, - onStartSearch = { - scope.launch(Dispatchers.Main) { - navController.navigate(PharmacyNavigationScreens.List.path()) - handleSearchResultFn( - pharmacySearchController.search(name = "", filter = searchFilter) - ) - } - }, - onShowMaps = { - scope.launch(Dispatchers.Main) { - navController.navigate(PharmacyNavigationScreens.Maps.path()) - handleSearchResultFn( - pharmacySearchController.search( - name = "", - filter = searchFilter.copy(nearBy = true) - ) - ) - } - }, - onSelectPharmacy = { pharmacy, orderOption -> - scope.launch(Dispatchers.Main) { - pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) - navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) - } - } - ) - } - } - composable(PharmacyNavigationScreens.List.route) { - NavigationAnimation(mode = navigationMode) { - PharmacySearchResultScreen( - pharmacyOrderController = pharmacyOrderController, - navController = navController, - searchController = pharmacySearchController, - onBack = { - pharmacyOrderController.onResetPharmacySelection() - navController.navigate(PharmacyNavigationScreens.StartSearch.path()) - }, - onClickMaps = { - scope.launch(Dispatchers.Main) { - navController.navigate(PharmacyNavigationScreens.Maps.path()) - handleSearchResultFn( - pharmacySearchController.search( - name = "", - filter = searchFilter.copy(nearBy = true) - ) - ) - } - }, - onSelectPharmacy = { pharmacy, orderOption -> - scope.launch(Dispatchers.Main) { - pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) - navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) - } - } - ) - } - } - - composable(PharmacyNavigationScreens.Maps.route) { - NavigationAnimation(mode = navigationMode) { - MapsOverview( - searchController = pharmacySearchController, - pharmacyOrderController = pharmacyOrderController, - navController = navController, - onBack = { - pharmacyOrderController.onResetPharmacySelection() - navController.popBackStack() - }, - onSelectPharmacy = { pharmacy, orderOption -> - scope.launch { - pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) - navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) - } - } - ) - } - } - composable(PharmacyNavigationScreens.OrderOverview.route) { - NavigationAnimation(mode = navigationMode) { - val mainScreenController = rememberMainScreenController() - OrderOverview( - pharmacyOrderController = pharmacyOrderController, - onClickContacts = { - navController.navigate(PharmacyNavigationScreens.EditShippingContact.path()) - }, - onBack = { navController.popBackStack() }, - onSelectPrescriptions = { - navController.navigate(PharmacyNavigationScreens.PrescriptionSelection.path()) - }, - onFinish = { hasError -> - mainScreenController.onOrdered(hasError = hasError) - onFinish() - } - ) - } - } - composable(PharmacyNavigationScreens.EditShippingContact.route) { - NavigationAnimation(mode = navigationMode) { - EditShippingContactScreen( - pharmacyOrderController = pharmacyOrderController, - onBack = { - navController.popBackStack() - } - ) - } - } - composable(PharmacyNavigationScreens.PrescriptionSelection.route) { - NavigationAnimation(mode = navigationMode) { - PrescriptionSelection( - orderState = pharmacyOrderController, - onFinishSelection = { navController.popBackStack() }, - onBack = { navController.popBackStack() } - ) - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt deleted file mode 100644 index c5f456d1..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt +++ /dev/null @@ -1,602 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.content.Context -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.material.SnackbarHost -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Mail -import androidx.compose.material.icons.outlined.Phone -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onPlaced -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.RedeemPrescriptionsController -import de.gematik.ti.erp.app.pharmacy.presentation.rememberRedeemPrescriptionsController -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isContactInformationMissing -import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isValid -import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.NavigationBack -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.letNotNull -import kotlinx.coroutines.launch -import java.util.UUID - -private const val OrderSuccessVideoAspectRatio = 1.69f -val TopBarColor = Color(0xffd6e9fb) - -@Requirement( - "A_19183#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Displays a summary of a prescription assignment to a pharmacy." -) -@Composable -fun OrderOverview( - pharmacyOrderController: PharmacyOrderController, - onClickContacts: () -> Unit, - onSelectPrescriptions: () -> Unit, - onBack: () -> Unit, - onFinish: (Boolean) -> Unit -) { - val orderState by pharmacyOrderController.orderState - val selectedPharmacy = remember { pharmacyOrderController.selectedPharmacy!! } - val selectedOrderOption = remember { pharmacyOrderController.selectedOrderOption!! } - - val listState = rememberLazyListState() - val videoHeightPx = remember { mutableFloatStateOf(0f) } - - val shippingContactState = remember(orderState, selectedOrderOption) { - pharmacyOrderController.shippingContactState(orderState.contact, selectedOrderOption) - } - - val scaffoldState = rememberScaffoldState() - Scaffold( - modifier = Modifier.testTag(TestTag.PharmacySearch.OrderSummary.Screen), - scaffoldState = scaffoldState, - snackbarHost = { - SnackbarHost(it, modifier = Modifier.systemBarsPadding()) - } - ) { padding -> - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth().padding(padding), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) - ) { - item { - val shape = RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp) - Box { - VideoContent( - Modifier - .onPlaced { - videoHeightPx.floatValue = it.size.height.toFloat() - } - .clip(shape) - .background(TopBarColor) - .statusBarsPadding() - .fillMaxWidth(), - source = when (selectedOrderOption) { - PharmacyScreenData.OrderOption.PickupService -> R.raw.animation_local - PharmacyScreenData.OrderOption.CourierDelivery -> R.raw.animation_courier - PharmacyScreenData.OrderOption.MailDelivery -> R.raw.animation_mail - }, - aspectRatioOverwrite = OrderSuccessVideoAspectRatio - ) - - NavigationBack(onClick = onBack) - } - } - item { - SpacerMedium() - Text( - stringResource(R.string.pharmacy_order_title), - textAlign = TextAlign.Center, - style = AppTheme.typography.h5, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - ) - } - item { - Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { - Text(stringResource(R.string.pharmacy_order_receiver), style = AppTheme.typography.h6) - SpacerMedium() - ContactSelectionButton( - contact = orderState.contact, - shippingContactState = shippingContactState, - onClick = onClickContacts - ) - } - } - item { - Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { - Text(stringResource(R.string.pharmacy_order_prescriptions), style = AppTheme.typography.h6) - SpacerMedium() - orderState.orders.takeIf { it.isNotEmpty() }?.let { - PrescriptionSelectionButton( - prescriptions = it, - onClick = onSelectPrescriptions - ) - } - } - } - item { - Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { - Text(stringResource(R.string.pharmacy_order_pharmacy), style = AppTheme.typography.h6) - SpacerMedium() - PharmacySelectionButton( - selectedPharmacy = selectedPharmacy, - selectedOrderOption = selectedOrderOption, - onClick = onBack - ) - SpacerMedium() - } - } - item { - RedeemButton( - pharmacyOrderController = pharmacyOrderController, - scaffoldState = scaffoldState, - shippingContactCompleted = - shippingContactState == ShippingContactState.ValidShippingContactState.OK, - onFinish = onFinish - ) - } - } - } -} - -@Composable -private fun RedeemButton( - pharmacyOrderController: PharmacyOrderController, - scaffoldState: ScaffoldState, - shippingContactCompleted: Boolean, - onFinish: (Boolean) -> Unit -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val redeemController = rememberRedeemPrescriptionsController() - - val profile by pharmacyOrderController.activeProfileState - - val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState - - val order by pharmacyOrderController.orderState - - // TODO : Remove !! and refactor - val selectedPharmacy = remember { pharmacyOrderController.selectedPharmacy } - - // TODO : Remove !! and refactor - val selectedOrderOption = remember { pharmacyOrderController.selectedOrderOption } - - var uploadInProgress by remember { mutableStateOf(false) } - - var showDialog by remember { mutableStateOf(false) } - var dialogTitle by remember { mutableStateOf("") } - var dialogDescription by remember { mutableStateOf("") } - - if (showDialog) { - PrescriptionRedeemAlertDialog( - title = dialogTitle, - description = dialogDescription, - onDismiss = { - showDialog = false - onFinish(true) - } - ) - } - - Surface( - modifier = Modifier.fillMaxWidth(), - elevation = 4.dp - ) { - Column(Modifier.navigationBarsPadding()) { - SpacerMedium() - PrimaryButtonLarge( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .testTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton), - enabled = shippingContactCompleted && !uploadInProgress, - onClick = { - uploadInProgress = true - scope.launch { - letNotNull(selectedOrderOption, selectedPharmacy) { option, pharmacy -> - try { - val redeemState = if (directRedeemEnabled) { - redeemController.orderPrescriptionsDirectly( - orderId = UUID.randomUUID(), - prescriptions = order.orders, - redeemOption = option, - pharmacy = pharmacy, - contact = order.contact - ) - } else { - redeemController - .orderPrescriptions( - profileId = profile.id, - orderId = UUID.randomUUID(), - prescriptions = order.orders, - redeemOption = option, - pharmacy = pharmacy, - contact = order.contact - ) - } - when (redeemState) { - is PrescriptionServiceErrorState -> { - redeemErrorMessage(context, redeemState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - - is RedeemPrescriptionsController.State.Ordered -> { - val responseCodeMessagesMap = responseCodeMessagesMap(context) - - val results = redeemState.results.values - when { - results.size == 1 -> { - // case 1: When one prescription is transferred. - val responseMessagesPairedList: List> = - results.mapNotNull { - responseCodeMessagesMap[it as PrescriptionServiceState] - } - dialogTitle = responseMessagesPairedList.joinToString { it.first ?: "" } - dialogDescription = - responseMessagesPairedList.joinToString { it.second ?: "" } - } - - results.contains(RedeemPrescriptionsController.State.Success.Ok) -> { - // case 2.1: When multiple prescriptions are transferred Successfully. - dialogTitle = context.getString(R.string.server_return_code_200_title) - dialogDescription = context.getString(R.string.server_return_code_200) - } - - else -> { - // case 2.2: When any multiple prescription are transferred Unsuccessfully. Show a generic error message. - dialogTitle = context.getString( - R.string.server_return_code_title_failure - ) - dialogDescription = context.getString(R.string.several_return_code) - } - } - showDialog = true - } - - else -> {} - } - } finally { - uploadInProgress = false - } - } - } - } - ) { - Text(stringResource(R.string.pharmacy_order_send)) - } - } - } -} - -fun responseCodeMessagesMap(context: Context): Map> { - // Mapping server code responses to the Title & Description used in Alert Dialog. - return mapOf( - RedeemPrescriptionsController.State.Success.Ok to Pair( - context.getString(R.string.server_return_code_200_title), - context.getString(R.string.server_return_code_200) - ), // 200, 201 - RedeemPrescriptionsController.State.Error.IncorrectDataStructure to Pair( - context.getString(R.string.server_return_code_400_title), - context.getString(R.string.server_return_code_400) - ), // 400 - RedeemPrescriptionsController.State.Error.JsonViolated to Pair( - context.getString(R.string.server_return_code_title_failure), - context.getString(R.string.server_return_code_401) - ), // 401 - RedeemPrescriptionsController.State.Error.UnableToRedeem to Pair( - context.getString(R.string.server_return_code_title_failure), - context.getString(R.string.server_return_code_404) - ), // 404 - RedeemPrescriptionsController.State.Error.Timeout to Pair( - context.getString(R.string.server_return_code_408_title), - context.getString(R.string.server_return_code_408) - ), // 408 - RedeemPrescriptionsController.State.Error.Conflict to Pair( - context.getString(R.string.server_return_code_409_title), - context.getString(R.string.server_return_code_409) - ), // 409 - RedeemPrescriptionsController.State.Error.Gone to Pair( - context.getString(R.string.server_return_code_410_title), - context.getString(R.string.server_return_code_410) - ), // 410 - RedeemPrescriptionsController.State.Error.Unknown to Pair( - context.getString(R.string.server_return_no_code_title), - context.getString(R.string.server_return_no_code) - ) // No error - ) -} - -@Composable -fun PrescriptionRedeemAlertDialog( - title: String, - description: String, - onDismiss: () -> Unit -) { - AcceptDialog( - header = title, - info = description, - onClickAccept = { - onDismiss() - }, - acceptText = stringResource(R.string.pharmacy_search_apovz_call_failed_accept) - ) -} - -@Composable -private fun ContactSelectionButton( - contact: PharmacyUseCaseData.ShippingContact, - onClick: () -> Unit, - shippingContactState: ShippingContactState -) { - FlatButton( - onClick = onClick - ) { - if (contact.isEmpty()) { - Text( - stringResource(R.string.pharmacy_order_add_contacts), - style = AppTheme.typography.subtitle1, - color = AppTheme.colors.primary600 - ) - } else { - Row(verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Column { - if (contact.name.isNotBlank()) { - Text(contact.name, style = AppTheme.typography.subtitle1) - } - contact.address().forEach { - Text(it, style = AppTheme.typography.body1l) - } - } - if (contact.other().isNotEmpty()) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - if (contact.telephoneNumber.isNotBlank()) { - SmallChip(Icons.Outlined.Phone, contact.telephoneNumber) - } - if (contact.mail.isNotBlank()) { - SmallChip(Icons.Outlined.Mail, contact.mail) - } - } - } - if (contact.deliveryInformation.isNotBlank()) { - Text(contact.deliveryInformation, style = AppTheme.typography.body1l) - } - } - if (!shippingContactState.isValid()) { - val text = if (shippingContactState.isContactInformationMissing()) { - stringResource(R.string.pharmacy_order_further_contact_information_required) - } else { - stringResource(R.string.pharmacy_order_contact_information_invalid) - } - SpacerSmall() - Surface(shape = RoundedCornerShape(8.dp), color = AppTheme.colors.red100) { - Text( - text, - color = AppTheme.colors.red900, - style = AppTheme.typography.subtitle2, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) - ) - } - } - } - SpacerMedium() - Text( - stringResource(R.string.pharmacy_order_change_contacts), - style = AppTheme.typography.subtitle2, - color = AppTheme.colors.primary600 - ) - } - } - } -} - -@Composable -private fun PrescriptionSelectionButton( - prescriptions: List, - onClick: () -> Unit -) { - val titlePrepend = stringResource(R.string.pres_details_scanned_medication) - - val (title, desc) = when (prescriptions.size) { - 1 -> - Pair( - prescriptions.first().title ?: "$titlePrepend ${prescriptions.first().index}", - null - ) - - else -> Pair( - stringResource(R.string.pharmacy_order_nr_of_prescriptions, prescriptions.size), - prescriptions.joinToString { it.title ?: "$titlePrepend ${it.index}" } - ) - } - - FlatButton( - modifier = Modifier.testTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton), - onClick = onClick - ) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - Text(title, style = AppTheme.typography.subtitle1) - desc?.let { - SpacerTiny() - Text(desc, style = AppTheme.typography.body2l, maxLines = 1, overflow = TextOverflow.Ellipsis) - } - } - SpacerMedium() - Icon(Icons.Rounded.KeyboardArrowRight, null) - } - } -} - -@Composable -private fun SmallChip( - icon: ImageVector, - text: String -) = - Surface(shape = RoundedCornerShape(8.dp), color = AppTheme.colors.neutral100) { - Row( - Modifier.padding(horizontal = PaddingDefaults.Small, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(icon, null, tint = AppTheme.colors.neutral500) - SpacerSmall() - Text( - text, - style = AppTheme.typography.body1 - ) - } - } - -@Composable -private fun PharmacySelectionButton( - selectedPharmacy: PharmacyUseCaseData.Pharmacy, - selectedOrderOption: PharmacyScreenData.OrderOption, - onClick: () -> Unit -) { - FlatButton( - onClick = onClick - ) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Column(Modifier.weight(1f)) { - Text( - selectedPharmacy.name, - style = AppTheme.typography.subtitle1, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - SpacerTiny() - Text( - selectedPharmacy.singleLineAddress(), - style = AppTheme.typography.body2l, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - SpacerShortMedium() - ServiceOption( - option = selectedOrderOption - ) - } - SpacerMedium() - Text( - stringResource(R.string.pharmacy_order_change_order), - style = AppTheme.typography.subtitle2, - color = AppTheme.colors.primary600 - ) - } - } -} - -@Composable -private fun ServiceOption( - option: PharmacyScreenData.OrderOption -) { - val text = when (option) { - PharmacyScreenData.OrderOption.PickupService -> stringResource(R.string.pharmacy_order_collect) - PharmacyScreenData.OrderOption.CourierDelivery -> stringResource(R.string.pharmacy_order_delivery) - PharmacyScreenData.OrderOption.MailDelivery -> stringResource(R.string.pharmacy_order_mail) - } - val shape = RoundedCornerShape(8.dp) - Box( - Modifier - .background(AppTheme.colors.green200, shape) - .padding(horizontal = PaddingDefaults.ShortMedium, vertical = PaddingDefaults.ShortMedium / 2) - ) { - Text(text, style = AppTheme.typography.subtitle2, color = AppTheme.colors.green900) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun FlatButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, - content: @Composable () -> Unit -) = - Surface( - modifier = modifier.fillMaxWidth(), - onClick = onClick, - shape = RoundedCornerShape(16.dp), - color = AppTheme.colors.neutral025, - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - elevation = 0.dp - ) { - Box(Modifier.padding(PaddingDefaults.Medium)) { - content() - } - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt deleted file mode 100644 index 8e00c6d2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.deliveryUrlNotEmpty -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isDeliveryWithoutContactUrls -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isOnlineServiceWithoutContactUrls -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.onlineUrlNotEmpty -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.pickupUrlNotEmpty -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.shortToast - -private const val MAX_OPTIONS = 3 -private const val DISABLED_ALPHA = 0.3f -private const val ENABLED_ALPHA = 1f - -@Composable -internal fun OrderSelection( - pharmacy: Pharmacy, - pharmacyOrderController: PharmacyOrderController, - onOrderClicked: (Pharmacy, PharmacyScreenData.OrderOption) -> Unit -) { - val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState - - val directRedeemUrlsNotPresent = pharmacy.directRedeemUrlsNotPresent - - val (pickUpContactAvailable, deliveryContactAvailable, onlineContactAvailable) = - pharmacy.checkRedemptionAndContactAvailabilityForPharmacy(directRedeemEnabled) - - val (pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) = - pharmacy.checkServiceVisibility( - directRedeemUrlsNotPresent = directRedeemUrlsNotPresent, - deliveryServiceAvailable = deliveryContactAvailable, - onlineServiceAvailable = onlineContactAvailable - ) - - val (pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) = - pharmacy.checkServiceAvailability( - directRedeemEnabled = directRedeemEnabled, - pickUpContactAvailable = pickUpContactAvailable, - deliveryContactAvailable = deliveryContactAvailable, - onlineContactAvailable = onlineContactAvailable - ) - - val numberOfServices = remember(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) { - listOf(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible).count { it } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - modifier = Modifier.height(IntrinsicSize.Min) - ) { - val modifier = Modifier.weight(weight = 0.5f).fillMaxHeight() - - if (pickUpServiceVisible) { - OrderButton( - modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), - isServiceEnabled = pickupServiceEnabled, - isLarge = numberOfServices != MAX_OPTIONS, - text = stringResource(R.string.pharmacy_order_opt_collect), - image = painterResource(R.drawable.pharmacy_small), - onClick = { - onOrderClicked( - pharmacy, - PharmacyScreenData.OrderOption.PickupService - ) - } - ) - } - - if (deliveryServiceVisible) { - OrderButton( - modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), - isServiceEnabled = deliveryServiceEnabled, - isLarge = numberOfServices != MAX_OPTIONS, - text = stringResource(R.string.pharmacy_order_opt_delivery), - image = painterResource(R.drawable.delivery_car_small), - onClick = { - onOrderClicked( - pharmacy, - PharmacyScreenData.OrderOption.CourierDelivery - ) - } - ) - } - if (onlineServiceVisible) { - OrderButton( - modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton), - isServiceEnabled = onlineServiceEnabled, - isLarge = numberOfServices != MAX_OPTIONS, - text = stringResource(R.string.pharmacy_order_opt_mail), - image = painterResource(R.drawable.truck_small), - onClick = { onOrderClicked(pharmacy, PharmacyScreenData.OrderOption.MailDelivery) } - ) - } - - if (numberOfServices == 1) { - Spacer(Modifier.weight(weight = 0.5f)) - } - } -} - -@Composable -private fun OrderButton( - modifier: Modifier, - isServiceEnabled: Boolean, - isLarge: Boolean = true, - text: String, - image: Painter, - onClick: () -> Unit -) { - val shape = RoundedCornerShape(PaddingDefaults.Medium) - val serviceDisabledText = stringResource(R.string.connect_for_pharmacy_service) - var showToast by remember { mutableStateOf(false) } - - // set the toast to be false on every recomposition - LaunchedEffect(showToast) { - showToast = false - } - - Column( - modifier = modifier - .background(AppTheme.colors.neutral100, shape) - .clip(shape) - .clickable( - role = Role.Button, - onClick = { - when { - isServiceEnabled -> onClick() - else -> showToast = true - } - } - ) - .padding(PaddingDefaults.Medium) - .alpha( - when { - isServiceEnabled -> ENABLED_ALPHA - else -> DISABLED_ALPHA - } - ) - ) { - val imgModifier = when { - isLarge -> Modifier.align(Alignment.End) - else -> Modifier.align(Alignment.CenterHorizontally) - } - Image(image, null, modifier = imgModifier) - SpacerTiny() - - val txtModifier = when { - isLarge -> Modifier.align(Alignment.Start) - else -> Modifier.align(Alignment.CenterHorizontally) - } - Text(text, modifier = txtModifier, style = AppTheme.typography.subtitle2) - } - - AnimatedVisibility(showToast) { - shortToast(serviceDisabledText) - } -} - -private fun Pharmacy.checkRedemptionAndContactAvailabilityForPharmacy( - directRedeemEnabled: Boolean -): Triple { - val pickUpServiceAvailable = directRedeemEnabled && pickupUrlNotEmpty() - - val deliveryServiceAvailable = directRedeemEnabled && deliveryUrlNotEmpty() - - val onlineServiceAvailable = directRedeemEnabled && onlineUrlNotEmpty() - - return Triple(pickUpServiceAvailable, deliveryServiceAvailable, onlineServiceAvailable) -} - -private fun Pharmacy.checkServiceVisibility( - directRedeemUrlsNotPresent: Boolean, - deliveryServiceAvailable: Boolean, - onlineServiceAvailable: Boolean -): Triple { - val pickUpServiceVisible = pickupUrlNotEmpty() || directRedeemUrlsNotPresent - - val deliveryServiceVisible = deliveryServiceAvailable || - deliveryUrlNotEmpty() || - isDeliveryWithoutContactUrls(directRedeemUrlsNotPresent) - - val onlineServiceVisible = onlineServiceAvailable || - onlineUrlNotEmpty() || - isOnlineServiceWithoutContactUrls(directRedeemUrlsNotPresent) - - return Triple(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) -} - -private fun Pharmacy.checkServiceAvailability( - directRedeemEnabled: Boolean, - pickUpContactAvailable: Boolean, - deliveryContactAvailable: Boolean, - onlineContactAvailable: Boolean -): Triple { - val pickupServiceEnabled = pickupUrlNotEmpty() || - pickUpContactAvailable || - !directRedeemEnabled && isPickupService - - val deliveryServiceEnabled = deliveryUrlNotEmpty() || - deliveryContactAvailable || - !directRedeemEnabled && isDeliveryService - - val onlineServiceEnabled = onlineUrlNotEmpty() || - onlineContactAvailable || - !directRedeemEnabled && isOnlineService - - return Triple(pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt deleted file mode 100644 index 625822b1..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("TooManyFunctions") - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.style.TextOverflow -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.fhir.model.Location -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyController -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.canHandleIntent -import de.gematik.ti.erp.app.utils.compose.handleIntent -import de.gematik.ti.erp.app.utils.compose.provideEmailIntent -import de.gematik.ti.erp.app.utils.compose.providePhoneIntent -import kotlinx.coroutines.launch - -@Composable -fun PharmacyBottomSheetDetails( - orderController: PharmacyOrderController, - pharmacy: PharmacyUseCaseData.Pharmacy, - pharmacyPortalUri: String = stringResource(R.string.pharmacy_detail_pharmacy_portal_uri), - pharmacyPortalText: String = stringResource(R.string.pharmacy_detail_data_info_domain), - infoText: String = stringResource(R.string.pharmacy_detail_data_info), - faqUri: String = stringResource(R.string.pharmacy_detail_data_info_faqs_uri), - onClickOrder: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit -) { - val context = LocalContext.current - val uriHandler = LocalUriHandler.current - val scrollState = rememberScrollState() - val controller = rememberPharmacyController() - val scope = rememberCoroutineScope() - val styledText = buildStyledTextForPharmacy( - infoText = infoText, - pharmacyPortalUri = pharmacyPortalUri, - start = infoText.indexOf(pharmacyPortalText), - end = infoText.indexOf(pharmacyPortalText) + pharmacyPortalText.length - ) - val hasRedeemableOrders by orderController.hasRedeemableOrdersState - - var showNoRedeemableTasksDialog by remember { mutableStateOf(false) } - - if (showNoRedeemableTasksDialog) { - AcceptDialog( - header = stringResource(R.string.pharmacy_order_no_prescriptions_title), - info = stringResource(R.string.pharmacy_order_no_prescriptions_desc), - acceptText = stringResource(R.string.ok), - onClickAccept = { - showNoRedeemableTasksDialog = false - } - ) - } - Column( - modifier = Modifier - .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Large) - .verticalScroll(scrollState) - .fillMaxWidth() - .testTag(TestTag.PharmacySearch.OrderOptions.Content), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row { - Column( - modifier = Modifier - .weight(1f) - .clickable( - role = Role.Button, - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - pharmacy.location?.let { - navigateWithGoogleMaps(context, it) ?: launchMaps(context, it) - } - } - ) { - Text( - text = pharmacy.name, - style = AppTheme.typography.h6, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - SpacerTiny() - Text( - text = pharmacy.singleLineAddress(), - style = AppTheme.typography.subtitle2, - color = MaterialTheme.colors.secondary, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - val isMarkedAsFavorite by produceState(false, pharmacy) { - controller.isPharmacyInFavorites(pharmacy).collect { value = it } - } - SpacerMedium() - FavoriteStarButton( - isMarked = isMarkedAsFavorite, - modifier = Modifier, - onChange = { - scope.launch { - if (it) { - controller.markPharmacyAsFavorite(pharmacy) - } else { - controller.unmarkPharmacyAsFavorite(pharmacy) - } - } - } - ) - } - SpacerXXLarge() - OrderSelection( - pharmacyOrderController = orderController, - pharmacy = pharmacy, - onOrderClicked = { pharmacy: PharmacyUseCaseData.Pharmacy, option: PharmacyScreenData.OrderOption -> - if (!hasRedeemableOrders) { - showNoRedeemableTasksDialog = true - } else { - onClickOrder(pharmacy, option) - } - } - ) - SpacerXXLarge() - PharmacyContact( - openingHours = pharmacy.openingHours, - phone = pharmacy.contacts.phone, - mail = pharmacy.contacts.mail, - url = pharmacy.contacts.url, - detailedInfoText = styledText, - onPhoneClicked = { context.handleIntent(providePhoneIntent(it)) }, - onMailClicked = { emailAddress -> - val intent = provideEmailIntent(emailAddress) - if (canHandleIntent(intent, context.packageManager)) { - context.startActivity(intent) - } else { - // Should we do something here? - } - }, - onUrlClicked = { url -> uriHandler.openUri(url) }, - onTextClicked = { - styledText - .getStringAnnotations("URL", it, it) - .firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) - } - }, - onHintClicked = { uriHandler.openUri(faqUri) } - ) - } -} - -@Composable -private fun buildStyledTextForPharmacy( - infoText: String, - pharmacyPortalUri: String, - start: Int, - end: Int -) = with(AnnotatedString.Builder()) { - append(infoText) - addStringAnnotation( - tag = "URL", - annotation = pharmacyPortalUri, - start = start, - end = end - ) - addStyle( - SpanStyle(color = AppTheme.colors.primary600), - start, - end - ) - toAnnotatedString() -} - -private fun launchMaps(context: Context, location: Location) { - val gmmIntentUri = Uri.parse("geo:${location.latitude},${location.longitude}?z=16") - val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) - mapIntent.resolveActivity(context.packageManager)?.let { - context.startActivity(mapIntent) - } -} - -private fun navigateWithGoogleMaps( - context: Context, - location: Location -): Any? { - val gmmIntentUri = - Uri.parse("https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}") - val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) - mapIntent.setPackage("com.google.android.apps.maps") - return mapIntent.resolveActivity(context.packageManager)?.let { - context.startActivity(mapIntent) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt deleted file mode 100644 index aa72786b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.fhir.model.OpeningHours -import de.gematik.ti.erp.app.fhir.model.isOpenToday -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.ErezeptText -import de.gematik.ti.erp.app.utils.compose.HintTextActionButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.time.format.TextStyle -import java.util.Locale - -@Composable -internal fun PharmacyContact( - openingHours: OpeningHours?, - phone: String, - mail: String, - url: String, - detailedInfoText: AnnotatedString, - onPhoneClicked: (String) -> Unit, - onMailClicked: (String) -> Unit, - onUrlClicked: (String) -> Unit, - onTextClicked: (Int) -> Unit, - onHintClicked: () -> Unit -) { - Column { - openingHours?.let { - if (it.isNotEmpty()) { - PharmacyOpeningHours(it) - } - SpacerMedium() - } - if (phone.isNotEmpty() || mail.isNotEmpty() || url.isNotEmpty()) { - ErezeptText.Title(text = stringResource(id = R.string.legal_notice_contact_header)) - SpacerMedium() - } - if (phone.isNotEmpty()) { - ContactLabel( - text = phone, - label = stringResource(R.string.pres_detail_organization_label_telephone), - onClick = onPhoneClicked - ) - SpacerMedium() - } - if (mail.isNotEmpty()) { - ContactLabel( - text = mail, - label = stringResource(R.string.pres_detail_organization_label_email), - onClick = onMailClicked - ) - SpacerMedium() - } - if (url.isNotEmpty()) { - ContactLabel( - text = url, - label = stringResource(R.string.pharm_detail_website), - onClick = onUrlClicked - ) - SpacerMedium() - } - SpacerMedium() - DataInfoSection( - modifier = Modifier.align(Alignment.End), - detailedInfoText = detailedInfoText, - onTextClicked = onTextClicked, - onHintClicked = onHintClicked - ) - } -} - -@Composable -private fun PharmacyOpeningHours(openingHours: OpeningHours) { - val dateTimeFormatter = remember { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) } - - Column { - Text( - text = stringResource(R.string.pharm_detail_opening_hours), - style = AppTheme.typography.h6 - ) - - SpacerMedium() - - val sortedOpeningHours = OpeningHours(openingHours.toSortedMap(compareBy { it })) - - for (h in sortedOpeningHours) { - val (day, hours) = h - val now = - remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } - val isOpenToday = remember(now) { h.isOpenToday(now) } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = day.getDisplayName(TextStyle.FULL, Locale.getDefault()), - fontWeight = if (isOpenToday) FontWeight.Medium else null - ) - Column( - horizontalAlignment = Alignment.End - ) { - for (hour in hours.sortedBy { it.openingTime }) { - val opens = - hour.openingTime?.toJavaLocalTime()?.format(dateTimeFormatter) ?: "" - val closes = - hour.closingTime?.toJavaLocalTime()?.format(dateTimeFormatter) ?: "" - val text = "$opens - $closes" - val isOpenNow = - remember(now) { hour.isOpenAt(now.time) && isOpenToday } - when { - isOpenNow -> - Text( - text = text, - color = AppTheme.colors.green600, - fontWeight = FontWeight.Medium - ) - - isOpenToday -> - Text( - text = text, - color = AppTheme.colors.neutral600, - fontWeight = FontWeight.Medium - ) - - else -> - Text( - text = text, - color = AppTheme.colors.neutral600 - ) - } - } - } - } - SpacerMedium() - } - } -} - -@Composable -private fun DataInfoSection( - modifier: Modifier, - detailedInfoText: AnnotatedString, - onTextClicked: (Int) -> Unit, - onHintClicked: () -> Unit -) { - ClickableText( - modifier = modifier - .fillMaxWidth(), - text = detailedInfoText, - style = AppTheme.typography.body2l, - onClick = onTextClicked - ) - SpacerSmall() - Row(modifier = modifier) { - HintTextActionButton( - text = stringResource(R.string.pharmacy_detail_data_info_btn), - onClick = onHintClicked - ) - } -} - -@Composable -private fun ContactLabel( - text: String, - label: String, - onClick: (String) -> Unit -) { - Column( - modifier = Modifier - .clickable { - onClick(text) - } - .fillMaxWidth() - ) { - Text( - text = text, - style = AppTheme.typography.body1, - color = AppTheme.colors.primary600 - ) - SpacerTiny() - Text( - text = label, - style = AppTheme.typography.body2l - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyFilterSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyFilterSheetScreen.kt deleted file mode 100644 index f963ae57..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyFilterSheetScreen.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import com.google.accompanist.flowlayout.FlowRow -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.NearByFilterArgument -import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.ShowStartSearchButton -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController.FilterType -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.Chip -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar - -class PharmacyFilterSheetScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: PharmacyGraphController -) : PharmacyBottomSheetScreen() { - @Composable - override fun Content() { - val snackbar = LocalSnackbar.current - - val filter by graphController.filterState - - val isNearbyFilter = remember { navBackStackEntry.getNearbyFilter() } - - val showStartButton = remember { navBackStackEntry.getIsStartSearchButtonShown() } - - PharmacyFilterSheetScreenContent( - filter = filter, - isNearbyFilter = isNearbyFilter, - showStartButton = showStartButton, - onClickFilter = { - graphController.updateFilter(type = it) - }, - showToDo = { - snackbar.show("TODO start search with filters") - }, - onBack = { - navController.popBackStack() - } - ) - } -} - -@Composable -private fun PharmacyFilterSheetScreenContent( - filter: PharmacyUseCaseData.Filter, - isNearbyFilter: Boolean, - showStartButton: Boolean, - onClickFilter: (FilterType) -> Unit, - showToDo: () -> Unit, - onBack: () -> Unit -) { - Column( - modifier = Modifier.padding(PaddingDefaults.Medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - stringResource(R.string.search_pharmacies_filter_header), - style = AppTheme.typography.h5 - ) - IconButton( - modifier = Modifier - .background(AppTheme.colors.neutral200, CircleShape), - onClick = onBack - ) { - Icon(Icons.Rounded.Close, null) - } - } - SpacerMedium() - Column(modifier = Modifier.verticalScroll(rememberScrollState(), true)) { - FlowRow( - mainAxisSpacing = PaddingDefaults.Small, - crossAxisSpacing = PaddingDefaults.Small - ) { - if (isNearbyFilter) { - Chip( - stringResource(R.string.search_pharmacies_filter_nearby), - closable = false, - checked = filter.nearBy - ) { - onClickFilter(FilterType.NEARBY) - } - } - Chip( - stringResource(R.string.search_pharmacies_filter_open_now), - closable = false, - checked = filter.openNow - ) { - onClickFilter(FilterType.OPEN_NOW) - } - Chip( - stringResource(R.string.search_pharmacies_filter_delivery_service), - closable = false, - checked = filter.deliveryService - ) { - onClickFilter(FilterType.DELIVERY_SERVICE) - if (it) { - onClickFilter(FilterType.NEARBY) - } - } - Chip( - stringResource(R.string.search_pharmacies_filter_online_service), - closable = false, - checked = filter.onlineService - ) { - onClickFilter(FilterType.ONLINE_SERVICE) - } - } - SpacerMedium() - if (showStartButton) { - SpacerLarge() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - PrimaryButtonSmall( - onClick = { - onBack() - showToDo() - } - ) { - Text(stringResource(R.string.search_pharmacies_start_search)) - } - } - SpacerLarge() - } - } - } -} - -private fun NavBackStackEntry.getNearbyFilter(): Boolean = - arguments?.getBoolean(NearByFilterArgument) ?: false - -private fun NavBackStackEntry.getIsStartSearchButtonShown(): Boolean = - arguments?.getBoolean(ShowStartSearchButton) ?: false - -@LightDarkPreview -@Composable -fun PharmacyFilterSheetScreenPreview() { - PreviewAppTheme { - PharmacyFilterSheetScreenContent( - filter = PharmacyUseCaseData.Filter( - nearBy = true, - openNow = false, - deliveryService = false, - onlineService = true - ), - isNearbyFilter = true, - showStartButton = true, - onClickFilter = {}, - showToDo = {}, - onBack = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyImagePlaceholder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyImagePlaceholder.kt new file mode 100644 index 00000000..dabfc54c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyImagePlaceholder.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R + +@Composable +fun PharmacyImagePlaceholder(modifier: Modifier) { + Image( + painterResource(R.drawable.ic_green_cross), + null, + modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .size(64.dp) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt index ef586fe6..404cfb08 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.ui @@ -21,16 +21,11 @@ package de.gematik.ti.erp.app.pharmacy.ui import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy object PharmacyOrderExtensions { - internal fun Pharmacy.isPickupWithoutContactUrls(hasNoContactUrls: Boolean) = - hasNoContactUrls && isPickupService internal fun Pharmacy.isDeliveryWithoutContactUrls(hasNoContactUrls: Boolean) = hasNoContactUrls && isDeliveryService internal fun Pharmacy.isOnlineServiceWithoutContactUrls(hasNoContactUrls: Boolean) = hasNoContactUrls && isOnlineService - internal fun Pharmacy.pickupUrlEmpty() = contacts.pickUpUrl.isEmpty() internal fun Pharmacy.pickupUrlNotEmpty() = contacts.pickUpUrl.isNotEmpty() - internal fun Pharmacy.deliveryUrlEmpty() = contacts.deliveryUrl.isEmpty() internal fun Pharmacy.deliveryUrlNotEmpty() = contacts.deliveryUrl.isNotEmpty() - internal fun Pharmacy.onlineUrlEmpty() = contacts.onlineServiceUrl.isEmpty() internal fun Pharmacy.onlineUrlNotEmpty() = contacts.onlineServiceUrl.isNotEmpty() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt deleted file mode 100644 index 766dba55..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService -import de.gematik.ti.erp.app.fhir.model.LocalPharmacyService -import de.gematik.ti.erp.app.fhir.model.OnlinePharmacyService -import de.gematik.ti.erp.app.fhir.model.OpeningHours -import de.gematik.ti.erp.app.fhir.model.PharmacyContacts -import de.gematik.ti.erp.app.fhir.model.PickUpPharmacyService -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -@Composable -internal fun PharmacyResultCard( - modifier: Modifier, - pharmacy: Pharmacy, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .clickable(onClick = onClick) - .then(modifier), - verticalAlignment = Alignment.CenterVertically - ) { - val distanceTxt = pharmacy.distance?.let { distance -> - formattedDistance(distance) - } - - PharmacyImagePlaceholder(Modifier) - SpacerMedium() - Column(modifier = Modifier.weight(1f)) { - Text( - pharmacy.name, - style = AppTheme.typography.subtitle1 - ) - - Text( - pharmacy.singleLineAddress(), - style = AppTheme.typography.body2l, - modifier = Modifier, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - - val pharmacyLocalServices = - pharmacy.provides.find { it is LocalPharmacyService } as LocalPharmacyService - val now = - remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } - - if (pharmacyLocalServices.isOpenAt(now)) { - val text = if (pharmacyLocalServices.isAllDayOpen(now.dayOfWeek)) { - stringResource(R.string.search_pharmacy_continuous_open) - } else { - stringResource( - R.string.search_pharmacy_open_until, - requireNotNull(pharmacyLocalServices.openUntil(now)).toString() - ) - } - Text( - text, - style = AppTheme.typography.subtitle2l, - color = AppTheme.colors.green600 - ) - } else { - val text = - pharmacyLocalServices.opensAt(now)?.let { - stringResource( - R.string.search_pharmacy_opens_at, - it.toString() - ) - } - if (text != null) { - Text( - text, - style = AppTheme.typography.subtitle2l, - color = AppTheme.colors.yellow600 - ) - } - } - } - - SpacerMedium() - - if (distanceTxt != null) { - Text( - distanceTxt, - style = AppTheme.typography.body2l, - modifier = Modifier - .align(Alignment.CenterVertically), - textAlign = TextAlign.End - ) - } - Icon( - Icons.Rounded.KeyboardArrowRight, - null, - tint = AppTheme.colors.neutral400, - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterVertically) - ) - } -} - -@LightDarkPreview -@Composable -internal fun PharmacyResultCardPreview() { - AppTheme { - PharmacyResultCard( - modifier = Modifier, - pharmacy = Pharmacy( - id = "pharmacy-id", - name = "2Königen-Aptheke", - address = "Ostwall 97, 47798 Krefeld", - location = null, - distance = null, - contacts = PharmacyContacts( - phone = "12345678", - mail = "pharmacy@mail.com", - url = "https://pharmacy.com", - pickUpUrl = "https://pharmacy.pickup.com/code123", - deliveryUrl = "https://pharmacy.delivery.com/code123", - onlineServiceUrl = "https://pharmacy.online.com/code123" - ), - provides = listOf( - DeliveryPharmacyService( - name = "delivery-service", - openingHours = OpeningHours(openingTime = mapOf()) - ), - OnlinePharmacyService(name = "online-service"), - PickUpPharmacyService(name = "pickup-service"), - LocalPharmacyService( - name = "local-service", - openingHours = OpeningHours(openingTime = mapOf()) - ) - ), - openingHours = OpeningHours(openingTime = mapOf()), - telematikId = "telematikId" - ), - onClick = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyScreen.kt index a867bce5..ae6de3c7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyScreen.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.ui -import de.gematik.ti.erp.app.navigation.BottomSheetScreen import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController @@ -26,8 +25,3 @@ abstract class PharmacyScreen : Screen() { abstract val graphController: PharmacyGraphController } - -abstract class PharmacyBottomSheetScreen : BottomSheetScreen() { - - abstract val graphController: PharmacyGraphController -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt deleted file mode 100644 index d7b5e19b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.net.Uri -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.add -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.LocalShipping -import androidx.compose.material.icons.outlined.LocationOn -import androidx.compose.material.icons.outlined.Moped -import androidx.compose.material.icons.outlined.Tune -import androidx.compose.material.icons.rounded.Search -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController -import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchController -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.extensions.isGooglePlayServiceAvailable -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun PharmacyOverviewScreen( - isNestedNavigation: Boolean, - pharmacyOrderController: PharmacyOrderController, - navController: NavHostController, - onBack: () -> Unit, - onStartSearch: () -> Unit, - onShowMaps: () -> Unit, - filter: PharmacyUseCaseData.Filter, - onFilterChange: (PharmacyUseCaseData.Filter) -> Unit, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit -) { - val listState = rememberLazyListState() - val scope = rememberCoroutineScope() - val sheetState = rememberPharmacySheetState() - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - async { - if (sheetState.isVisible) { - analytics.trackPharmacySearchPopUps(sheetState.content) - } else { - analytics.onPopUpClosed() - navController.currentBackStackEntry?.destination?.route?.let { uri -> - val route = Uri.parse(uri).buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - } - } - - Box { - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.PharmacySearch.OverviewScreen), - listState = listState, - topBarTitle = stringResource(R.string.redeem_header), - navigationMode = if (isNestedNavigation) NavigationBarMode.Back else NavigationBarMode.Close, - onBack = onBack - ) { - val pharmacySearchController = rememberPharmacySearchController() - OverviewContent( - onSelectPharmacy = { - sheetState.show(PharmacySearchSheetContentState.PharmacySelected(it)) - }, - listState = listState, - onFilterChange = onFilterChange, - searchFilter = filter, - onStartSearch = onStartSearch, - pharmacySearchController = pharmacySearchController, - onShowFilter = { - sheetState.show(PharmacySearchSheetContentState.FilterSelected()) - }, - onShowMaps = onShowMaps - ) - } - - ModalBottomSheet( - sheetState = sheetState, - sheetContent = { - when (sheetState.content) { - is PharmacySearchSheetContentState.FilterSelected -> - FilterSheetContent( - modifier = Modifier.navigationBarsPadding(), - filter = filter, - onClickChip = onFilterChange, - onClickClose = { scope.launch { sheetState.animateTo(ModalBottomSheetValue.Hidden) } }, - extraContent = { - SpacerLarge() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - PrimaryButtonSmall( - onClick = { - scope.launch { sheetState.animateTo(ModalBottomSheetValue.Hidden) } - onStartSearch() - } - ) { - Text(stringResource(R.string.search_pharmacies_start_search)) - } - } - SpacerLarge() - } - ) - - is PharmacySearchSheetContentState.PharmacySelected -> - PharmacyBottomSheetDetails( - orderController = pharmacyOrderController, - pharmacy = (sheetState.content as PharmacySearchSheetContentState.PharmacySelected) - .pharmacy, - onClickOrder = { pharmacy, orderOption -> - onSelectPharmacy(pharmacy, orderOption) - } - ) - } - }, - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) } - ) - } -} - -@Composable -private fun OverviewContent( - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, - listState: LazyListState, - searchFilter: PharmacyUseCaseData.Filter, - onFilterChange: (PharmacyUseCaseData.Filter) -> Unit, - onStartSearch: () -> Unit, - pharmacySearchController: PharmacySearchController, - onShowFilter: () -> Unit, - onShowMaps: () -> Unit -) { - val overviewPharmacies by pharmacySearchController.overviewPharmaciesState - - val contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) - .add(WindowInsets(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Medium)).asPaddingValues() - - val context = LocalContext.current - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.PharmacySearch.OverviewContent), - state = listState, - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = contentPadding - ) { - item { - PharmacySearchButton( - modifier = Modifier - .padding(horizontal = PaddingDefaults.Medium) - .testTag(TestTag.PharmacySearch.TextSearchButton) - ) { - onFilterChange( - searchFilter.copy( - onlineService = false, - deliveryService = false, - openNow = false - ) - ) - onStartSearch() - } - } - if (context.isGooglePlayServiceAvailable()) { - item { - MapsSection(onShowMaps = onShowMaps) - } - } - item { - FilterSection( - filter = searchFilter, - onClick = onFilterChange, - onClickFilter = onShowFilter, - onStartSearch = onStartSearch - ) - } - if (overviewPharmacies.isNotEmpty()) { - OverviewPharmacies( - pharmacies = overviewPharmacies, - onSelectPharmacy = onSelectPharmacy, - pharmacySearchController = pharmacySearchController - ) - } - } -} - -@Composable -private fun MapsSection( - onShowMaps: () -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Text( - stringResource(R.string.pharmacy_maps_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium) - .padding(horizontal = PaddingDefaults.Medium) - ) - } - MapsOverviewSmall( - modifier = Modifier - .fillMaxWidth() - .height(186.dp) - .padding(horizontal = PaddingDefaults.Medium), - onClick = onShowMaps - ) -} - -@Suppress("FunctionName") -private fun LazyListScope.OverviewPharmacies( - pharmacies: List, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, - pharmacySearchController: PharmacySearchController -) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - ) { - Text( - text = stringResource(R.string.pharmacy_my_pharmacies_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium), - textAlign = TextAlign.Start - ) - } - } - items(pharmacies) { - FavoritePharmacyCard( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - overviewPharmacy = it, - onSelectPharmacy = onSelectPharmacy, - pharmacySearchController = pharmacySearchController - ) - SpacerMedium() - } -} - -@Composable -private fun FilterSection( - filter: PharmacyUseCaseData.Filter, - onClick: (PharmacyUseCaseData.Filter) -> Unit, - onStartSearch: () -> Unit, - onClickFilter: () -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Center - ) { - Text( - stringResource(R.string.search_pharmacies_filter_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium) - .padding(horizontal = PaddingDefaults.Medium), - textAlign = TextAlign.Start - ) - FilterButton( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - text = stringResource(R.string.search_pharmacies_filter_open_now_and_local), - icon = Icons.Outlined.LocationOn, - onClick = { - onClick( - filter.copy( - nearBy = true, - openNow = true, - deliveryService = false, - onlineService = false - ) - ) - onStartSearch() - } - ) - FilterButton( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - text = stringResource(R.string.search_pharmacies_filter_delivery_service), - icon = Icons.Outlined.Moped, - onClick = { - onClick( - filter.copy( - nearBy = true, - deliveryService = true, - onlineService = false, - openNow = false - ) - ) - onStartSearch() - } - ) - FilterButton( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - text = stringResource(R.string.search_pharmacies_filter_online_service), - icon = Icons.Outlined.LocalShipping, - onClick = { - onClick( - filter.copy( - nearBy = false, - onlineService = true, - deliveryService = false, - openNow = false - ) - ) - onStartSearch() - } - ) - FilterButton( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - text = stringResource(R.string.search_pharmacies_filter_by), - icon = Icons.Outlined.Tune, - onClick = onClickFilter - ) - } -} - -@Composable -private fun FilterButton( - modifier: Modifier, - text: String, - icon: ImageVector, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(role = Role.Button) { onClick() } - .then(modifier), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start - ) { - Icon( - icon, - null, - tint = AppTheme.colors.neutral600 - ) - SpacerMedium() - Text( - text, - modifier = Modifier.padding(vertical = PaddingDefaults.Medium), - color = AppTheme.colors.neutral900, - style = AppTheme.typography.body1, - fontWeight = FontWeight.W400 - ) - } -} - -@Composable -private fun PharmacySearchButton( - modifier: Modifier, - onStartSearch: () -> Unit -) { - Row( - modifier = modifier - .clip(RoundedCornerShape(16.dp)) - .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(16.dp)) - .clickable(role = Role.Button) { onStartSearch() } - .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.ShortMedium) - ) { - Icon( - Icons.Rounded.Search, - tint = AppTheme.colors.neutral600, - contentDescription = null - ) - SpacerSmall() - Text( - text = stringResource(R.string.pharmacy_start_search_text), - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(1f), - style = AppTheme.typography.body1, - color = AppTheme.colors.neutral600 - ) - } -} - -@Composable -fun PharmacyImagePlaceholder(modifier: Modifier) { - Image( - painterResource(R.drawable.ic_green_cross), - null, - modifier = modifier - .clip(RoundedCornerShape(4.dp)) - .size(64.dp) - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt deleted file mode 100644 index d18d177e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt +++ /dev/null @@ -1,868 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.content.Intent -import android.net.Uri -import android.provider.Settings -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.material.SnackbarDuration -import androidx.compose.material.SnackbarResult -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Map -import androidx.compose.material.icons.rounded.Tune -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavHostController -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey -import com.google.accompanist.flowlayout.FlowRow -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController -import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions -import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.pharmacyId -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.AlertDialog -import de.gematik.ti.erp.app.utils.compose.Chip -import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.text.DecimalFormat - -private const val OneKilometerInMeter = 1000 - -@Composable -private fun PharmacySearchErrorHint( - title: String, - subtitle: String, - action: String? = null, - onClickAction: (() -> Unit)? = null, - modifier: Modifier -) { - Box( - modifier = modifier - ) { - Column( - modifier = Modifier - .align(Alignment.Center) - .padding(PaddingDefaults.Medium), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - title, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - Text( - subtitle, - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - if (action != null && onClickAction != null) { - TextButton(onClick = onClickAction) { - Text(action) - } - } - } - } -} - -@Composable -fun NoLocationDialog( - onAccept: () -> Unit -) { - AcceptDialog( - header = stringResource(R.string.search_pharmacies_location_na_header), - info = stringResource(R.string.search_pharmacies_location_na_header_info), - acceptText = stringResource(R.string.search_pharmacies_location_na_header_okay), - onClickAccept = onAccept - ) -} - -@Composable -fun NoLocationServicesDialog( - onClose: () -> Unit -) { - val context = LocalContext.current - AlertDialog( - title = { Text(stringResource(R.string.search_pharmacies_location_na_header)) }, - onDismissRequest = {}, - text = { Text(stringResource(R.string.search_pharmacies_location_na_services)) }, - buttons = { - TextButton(onClick = onClose) { - Text(stringResource(R.string.cancel)) - } - TextButton(onClick = { - context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) - onClose() - }) { - Text(stringResource(R.string.search_pharmacies_location_na_settings)) - } - }, - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) - ) -} - -@Composable -private fun PharmacySearchInputField( - modifier: Modifier, - onBack: () -> Unit, - isLoading: Boolean, - searchValue: String, - onSearchChange: (String) -> Unit, - onSearch: (String) -> Unit -) { - var isLoadingStable by remember { mutableStateOf(isLoading) } - - LaunchedEffect(isLoading) { - delay(timeMillis = 330) - isLoadingStable = isLoading - } - - TextField( - value = searchValue, - onValueChange = onSearchChange, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - singleLine = true, - keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Search - ), - keyboardActions = KeyboardActions { - onSearch(searchValue) - }, - visualTransformation = VisualTransformation.None, - trailingIcon = { - Crossfade(isLoadingStable, animationSpec = tween(durationMillis = 550)) { - if (it) { - Box(Modifier.size(48.dp)) { - CircularProgressIndicator( - modifier = Modifier - .size(24.dp) - .align(Alignment.Center), - strokeWidth = 2.dp - ) - } - } else { - IconButton( - onClick = { onSearchChange("") } - ) { - Icon( - Icons.Rounded.Close, - contentDescription = null - ) - } - } - } - }, - leadingIcon = { - IconButton( - onClick = { onBack() } - ) { - Icon( - Icons.Rounded.ArrowBack, - contentDescription = null - ) - } - }, - shape = RoundedCornerShape(16.dp), - textStyle = AppTheme.typography.body1, - colors = TextFieldDefaults.textFieldColors( - textColor = AppTheme.colors.neutral900, - leadingIconColor = AppTheme.colors.neutral600, - trailingIconColor = AppTheme.colors.neutral600, - backgroundColor = AppTheme.colors.neutral100, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ) - ) -} - -@Composable -private fun FilterSection( - filter: PharmacyUseCaseData.Filter, - onClickChip: (PharmacyUseCaseData.Filter) -> Unit, - onClickFilter: () -> Unit -) { - val rowState = rememberLazyListState() - Row(modifier = Modifier.fillMaxWidth()) { - SpacerMedium() - Row( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .clickable { - onClickFilter() - } - .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(8.dp)) - .padding(horizontal = PaddingDefaults.Small, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.Tune, null, Modifier.size(16.dp), tint = AppTheme.colors.primary600) - SpacerSmall() - Text( - stringResource(R.string.search_pharmacies_filter), - style = AppTheme.typography.subtitle2, - color = AppTheme.colors.primary600 - ) - } - if (filter.isAnySet()) { - SpacerSmall() - LazyRow( - state = rowState, - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), - modifier = Modifier.fillMaxWidth() - ) { - if (filter.nearBy) { - item { - Chip( - stringResource(R.string.search_pharmacies_filter_nearby), - closable = true, - checked = false - ) { - onClickChip(filter.copy(nearBy = false)) - } - } - } - if (filter.openNow) { - item { - Chip( - stringResource(R.string.search_pharmacies_filter_open_now), - closable = true, - checked = false - ) { - onClickChip(filter.copy(openNow = false)) - } - } - } - if (filter.deliveryService) { - item { - Chip( - stringResource(R.string.search_pharmacies_filter_delivery_service), - closable = true, - checked = false - ) { - onClickChip(filter.copy(deliveryService = false)) - } - } - } - if (filter.onlineService) { - item { - Chip( - stringResource(R.string.search_pharmacies_filter_online_service), - closable = true, - checked = false - ) { - onClickChip(filter.copy(onlineService = false)) - } - } - } - item { - SpacerSmall() - } - } - } - } -} - -@Composable -fun FilterSheetContent( - modifier: Modifier, - extraContent: @Composable () -> Unit = {}, - filter: PharmacyUseCaseData.Filter, - onClickChip: (PharmacyUseCaseData.Filter) -> Unit, - onClickClose: () -> Unit, - showNearByFilter: Boolean = true -) { - var filterValue by remember(filter) { mutableStateOf(filter) } - - val onClickChipFn = { f: PharmacyUseCaseData.Filter -> - filterValue = f - onClickChip(f) - } - - Column( - modifier.padding(PaddingDefaults.Medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - stringResource(R.string.search_pharmacies_filter_header), - style = AppTheme.typography.h6 - ) - IconButton( - modifier = Modifier - .background(AppTheme.colors.neutral100, CircleShape), - onClick = onClickClose - ) { - Icon( - Icons.Rounded.Close, - null - ) - } - } - SpacerMedium() - Column(modifier = Modifier.verticalScroll(rememberScrollState(), true)) { - FlowRow( - mainAxisSpacing = PaddingDefaults.Small, - crossAxisSpacing = PaddingDefaults.Small - ) { - if (showNearByFilter) { - Chip( - stringResource(R.string.search_pharmacies_filter_nearby), - closable = false, - checked = filterValue.nearBy - ) { - onClickChipFn( - filterValue.copy( - nearBy = it - ) - ) - } - } - Chip( - stringResource(R.string.search_pharmacies_filter_open_now), - closable = false, - checked = filterValue.openNow - ) { - onClickChipFn( - filterValue.copy( - openNow = it - ) - ) - } - Chip( - stringResource(R.string.search_pharmacies_filter_delivery_service), - closable = false, - checked = filterValue.deliveryService - ) { - onClickChipFn( - filterValue.copy( - nearBy = if (it) true else filterValue.nearBy, - deliveryService = it - ) - ) - } - Chip( - stringResource(R.string.search_pharmacies_filter_online_service), - closable = false, - checked = filterValue.onlineService - ) { - onClickChipFn( - filterValue.copy( - onlineService = it - ) - ) - } - } - - extraContent() - } - } -} - -internal fun formattedDistance(distanceInMeters: Double): String { - val f = DecimalFormat() - return if (distanceInMeters < OneKilometerInMeter) { - f.maximumFractionDigits = 0 - f.format(distanceInMeters).toString() + " m" - } else { - f.maximumFractionDigits = 1 - f.format(distanceInMeters / OneKilometerInMeter).toString() + " km" - } -} - -@Composable -private fun ErrorRetryHandler( - searchPagingItems: LazyPagingItems, - scaffoldState: ScaffoldState -) { - val errorTitle = stringResource(R.string.search_pharmacy_error_title) - val errorAction = stringResource(R.string.search_pharmacy_error_action) - - LaunchedEffect(searchPagingItems.loadState) { - searchPagingItems.loadState.let { - val anyErr = it.append is LoadState.Error || - it.prepend is LoadState.Error || - it.refresh is LoadState.Error - if (anyErr && searchPagingItems.itemCount > 1) { - val result = - scaffoldState.snackbarHostState.showSnackbar( - errorTitle, - errorAction, - duration = SnackbarDuration.Short - ) - if (result == SnackbarResult.ActionPerformed) { - searchPagingItems.retry() - } - } - } - } -} - -@Requirement( - "A_20285", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Search results are displayed solely based on search term and user-set filters." -) -@Suppress("LongMethod") -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun PharmacySearchResultScreen( - pharmacyOrderController: PharmacyOrderController, - searchController: PharmacySearchController, - navController: NavHostController, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, - onClickMaps: () -> Unit, - onBack: () -> Unit -) { - val searchPagingItems = searchController.pharmacySearchFlow.collectAsLazyPagingItems() - - val scaffoldState = rememberScaffoldState() - - var searchName by remember(searchController.searchState.name) { - mutableStateOf(searchController.searchState.name) - } - var searchFilter by remember(searchController.searchState.filter) { - mutableStateOf(searchController.searchState.filter) - } - - ErrorRetryHandler( - searchPagingItems, - scaffoldState - ) - - val scope = rememberCoroutineScope() - - val locationPermissionLauncher = getLocationPermissionLauncher(scope, searchController, searchName, searchFilter) - - var showNoLocationDialog by remember { mutableStateOf(false) } - if (showNoLocationDialog) { - NoLocationDialog( - onAccept = { - scope.launch { - searchController.search( - name = searchName, - filter = searchFilter.copy(nearBy = false) - ) - } - showNoLocationDialog = false - } - ) - } - - var showNoLocationServicesDialog by remember { mutableStateOf(false) } - if (showNoLocationServicesDialog) { - NoLocationServicesDialog( - onClose = { - scope.launch { - searchController.search( - name = searchName, - filter = searchFilter.copy(nearBy = false) - ) - } - showNoLocationServicesDialog = false - } - ) - } - - val isLoading by remember { - derivedStateOf { - searchController.isLoading - } - } - - val focusManager = LocalFocusManager.current - - val sheetState = rememberPharmacySheetState( - pharmacyOrderController.selectedPharmacy?.let { - PharmacySearchSheetContentState.PharmacySelected(it) - } - ) - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - analytics.trackPharmacySearchPopUps(sheetState.content) - } else { - analytics.onPopUpClosed() - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - Box { - Scaffold( - modifier = Modifier - .systemBarsPadding() - .testTag(TestTag.PharmacySearch.ResultScreen), - floatingActionButton = { - Button( - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.neutral050, - contentColor = AppTheme.colors.primary600 - ), - modifier = Modifier.size(56.dp), - onClick = onClickMaps - ) { - Icon( - Icons.Rounded.Map, - contentDescription = null - ) - } - } - ) { innerPadding -> - Column(Modifier.padding(innerPadding)) { - SpacerMedium() - PharmacySearchInputField( - modifier = Modifier.testTag(TestTag.PharmacySearch.TextSearchField), - onBack = onBack, - isLoading = isLoading, - searchValue = searchName, - onSearchChange = { searchName = it }, - onSearch = { - focusManager.clearFocus() - scope.launch { - when (searchController.search(name = it, filter = searchFilter)) { - PharmacySearchController.SearchQueryResult.Send -> {} - PharmacySearchController.SearchQueryResult.NoLocationPermission -> { - showNoLocationDialog = true - } - - PharmacySearchController.SearchQueryResult.NoLocationServicesEnabled -> { - showNoLocationServicesDialog = true - } - - PharmacySearchController.SearchQueryResult.NoLocationFound -> { - searchFilter = searchFilter.copy(nearBy = false) - } - } - } - } - ) - SpacerSmall() - - FilterSection( - filter = searchFilter, - onClickChip = { - focusManager.clearFocus() - scope.launch { - searchController.search(name = searchName, filter = it) - } - }, - onClickFilter = { - focusManager.clearFocus() - sheetState.show(PharmacySearchSheetContentState.FilterSelected()) - } - ) - - SpacerSmall() - - SearchResultContent( - searchPagingItems = searchPagingItems, - onSelectPharmacy = { - sheetState.show(PharmacySearchSheetContentState.PharmacySelected(it)) - } - ) - } - } - - ModalBottomSheet( - sheetState = sheetState, - sheetContent = { - when (sheetState.content) { - is PharmacySearchSheetContentState.FilterSelected -> - FilterSheetContent( - modifier = Modifier.navigationBarsPadding(), - filter = searchFilter, - onClickChip = { - focusManager.clearFocus() - scope.launch { - when (searchController.search(name = searchName, filter = it)) { - PharmacySearchController.SearchQueryResult.Send -> {} - PharmacySearchController.SearchQueryResult.NoLocationPermission -> { - locationPermissionLauncher.launch(locationPermissions) - } - - PharmacySearchController.SearchQueryResult.NoLocationServicesEnabled -> { - showNoLocationServicesDialog = true - } - - PharmacySearchController.SearchQueryResult.NoLocationFound -> { - searchFilter = searchFilter.copy(nearBy = false) - } - } - } - }, - onClickClose = { scope.launch { sheetState.animateTo(ModalBottomSheetValue.Hidden) } } - ) - - is PharmacySearchSheetContentState.PharmacySelected -> - PharmacyBottomSheetDetails( - orderController = pharmacyOrderController, - pharmacy = - (sheetState.content as PharmacySearchSheetContentState.PharmacySelected).pharmacy, - onClickOrder = { pharmacy, orderOption -> - onSelectPharmacy(pharmacy, orderOption) - } - ) - } - }, - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) } - ) - } -} - -@Composable -private fun getLocationPermissionLauncher( - scope: CoroutineScope, - searchController: PharmacySearchController, - searchName: String, - searchFilter: PharmacyUseCaseData.Filter -) = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - scope.launch { - searchController.search( - name = searchName, - filter = searchFilter.copy(nearBy = permissions.values.any { it }) - ) - } -} - -@Composable -private fun SearchResultContent( - searchPagingItems: LazyPagingItems, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit -) { - val errorTitle = stringResource(R.string.search_pharmacy_error_title) - val errorSubtitle = stringResource(R.string.search_pharmacy_error_subtitle) - val errorAction = stringResource(R.string.search_pharmacy_error_action) - - val modifier = Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium) - val loadState = searchPagingItems.loadState - - val showNothingFound by remember { - derivedStateOf { - listOf(loadState.prepend, loadState.append) - .all { - when (it) { - is LoadState.NotLoading -> - it.endOfPaginationReached && searchPagingItems.itemCount == 0 - - else -> false - } - } && loadState.refresh is LoadState.NotLoading - } - } - - val showError by remember { - derivedStateOf { searchPagingItems.itemCount <= 1 && loadState.refresh is LoadState.Error } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.PharmacySearch.ResultContent), - state = rememberLazyListState(), - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) - .asPaddingValues() - ) { - if (showNothingFound) { - item { - PharmacySearchErrorHint( - title = stringResource(R.string.search_pharmacy_nothing_found_header), - subtitle = stringResource(R.string.search_pharmacy_nothing_found_info), - modifier = Modifier - .fillMaxWidth() - .fillParentMaxHeight() - ) - } - } - if (showError) { - item { - PharmacySearchErrorHint( - title = errorTitle, - subtitle = errorSubtitle, - action = errorAction, - onClickAction = { searchPagingItems.retry() }, - modifier = Modifier - .fillMaxWidth() - .fillParentMaxHeight() - ) - } - } - if (loadState.prepend is LoadState.Error) { - item { - PharmacySearchErrorHint( - title = errorTitle, - subtitle = errorSubtitle, - action = errorAction, - onClickAction = { searchPagingItems.retry() }, - modifier = Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium) - ) - } - } - items( - count = searchPagingItems.itemCount, - key = searchPagingItems.itemKey { it.id } - ) { index -> - val pharmacy = searchPagingItems[index] - if (pharmacy != null) { - PharmacySearchResult( - modifier = modifier, - count = searchPagingItems.itemCount, - index = index, - pharmacy = pharmacy, - onSelectPharmacy = onSelectPharmacy - ) - } - } - if (loadState.append is LoadState.Error) { - item { - PharmacySearchErrorHint( - title = errorTitle, - subtitle = errorSubtitle, - action = errorAction, - onClickAction = { searchPagingItems.retry() }, - modifier = Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium) - ) - } - } - } -} - -@Composable -fun PharmacySearchResult( - modifier: Modifier, - count: Int, - index: Int, - pharmacy: PharmacyUseCaseData.Pharmacy, - onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit -) { - Column { - PharmacyResultCard( - modifier = modifier - .semantics { - pharmacyId = pharmacy.telematikId - } - .testTag(TestTag.PharmacySearch.PharmacyListEntry), - pharmacy = pharmacy - ) { - onSelectPharmacy(pharmacy) - } - Divider(startIndent = PaddingDefaults.Medium) - if (index < count - 1) { - Divider(startIndent = PaddingDefaults.Medium) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySheetState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySheetState.kt deleted file mode 100644 index eb55e1ed..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySheetState.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.SwipeableDefaults -import androidx.compose.material.SwipeableState -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Stable -class PharmacySheetState( - content: PharmacySearchSheetContentState -) : SwipeableState( - initialValue = ModalBottomSheetValue.Hidden, - animationSpec = SwipeableDefaults.AnimationSpec, - confirmStateChange = { true } -) { - lateinit var scope: CoroutineScope - - var content: PharmacySearchSheetContentState by mutableStateOf(content) - private set - - val isVisible: Boolean - get() = this.currentValue != ModalBottomSheetValue.Hidden - - fun show(content: PharmacySearchSheetContentState, snap: Boolean = false) { - this.content = content - scope.launch { - val state = when (content) { - is PharmacySearchSheetContentState.FilterSelected -> ModalBottomSheetValue.Expanded - is PharmacySearchSheetContentState.PharmacySelected -> ModalBottomSheetValue.HalfExpanded - } - if (snap) { - snapTo(state) - } else { - animateTo(state) - } - } - } - - fun hide() { - scope.launch { - animateTo(ModalBottomSheetValue.Hidden) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyStartScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyStartScreen.kt deleted file mode 100644 index 7ab0d077..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyStartScreen.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy -import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController -import de.gematik.ti.erp.app.pharmacy.ui.components.FavouritePharmacies -import de.gematik.ti.erp.app.pharmacy.ui.components.FilterSection -import de.gematik.ti.erp.app.pharmacy.ui.components.MapsTile -import de.gematik.ti.erp.app.pharmacy.ui.components.MapsTitle -import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacySearchButton -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar -import de.gematik.ti.erp.app.utils.extensions.capitalizeFirstChar -import de.gematik.ti.erp.app.utils.extensions.isGooglePlayServiceAvailable -import kotlinx.datetime.Instant - -class PharmacyStartScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry, - override val graphController: PharmacyGraphController -) : PharmacyScreen() { - @Composable - override fun Content() { - val snackbar = LocalSnackbar.current - - val context = LocalContext.current - - val filter by graphController.filterState - - val favouritePharmacies by graphController.favouritePharmaciesState - - val isGooglePlayServicesAvailable = context.isGooglePlayServiceAvailable() - - val listState = rememberLazyListState() - - LaunchedEffect(Unit) { - graphController.init() - graphController.updateIsDirectRedeemEnabledOnFilter() - } - - PharmacyStartScreenContent( - filter = filter, - pharmacies = favouritePharmacies, - listState = listState, - isGooglePlayServicesAvailable = isGooglePlayServicesAvailable, - onBack = { navController.popBackStack() }, - onClickPharmacySearch = { - snackbar.show("TODO open pharmacy search") - }, - onClickMapsSearch = { - snackbar.show("TODO open pharmacy maps") - }, - onClickQuickFilterSearch = { - snackbar.show("TODO open pharmacy search with filters") - }, - onClickFilter = { - navController.navigate( - PharmacyRoutes.PharmacyFilterSheetScreen.path( - showNearbyFilter = true, - showButton = true - ) - ) - }, - onClickFavouritePharmacy = { - snackbar.show("TODO open pharmacy detail bottomsheet") - } - ) - } -} - -@Composable -private fun PharmacyStartScreenContent( - filter: PharmacyUseCaseData.Filter = PharmacyUseCaseData.Filter(), - pharmacies: List = emptyList(), - listState: LazyListState, - isGooglePlayServicesAvailable: Boolean = true, - onClickQuickFilterSearch: (PharmacyUseCaseData.Filter) -> Unit, - onClickFavouritePharmacy: (OverviewPharmacy) -> Unit, - onClickPharmacySearch: () -> Unit, - onClickMapsSearch: () -> Unit, - onClickFilter: () -> Unit, - onBack: () -> Unit -) { - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.redeem_header).capitalizeFirstChar(), - listState = listState, - actions = {}, - onBack = onBack, - content = { paddingValues -> - PharmacyStartScreenBody( - paddingValues = paddingValues, - filter = filter, - pharmacies = pharmacies, - listState = listState, - isGooglePlayServicesAvailable = isGooglePlayServicesAvailable, - onClickPharmacySearch = onClickPharmacySearch, - onClickMapsSearch = onClickMapsSearch, - onClickQuickFilterSearch = onClickQuickFilterSearch, - onClickFilter = onClickFilter, - onClickFavouritePharmacy = onClickFavouritePharmacy - ) - } - ) -} - -@Composable -private fun PharmacyStartScreenBody( - paddingValues: PaddingValues, - filter: PharmacyUseCaseData.Filter, - pharmacies: List, - listState: LazyListState, - isGooglePlayServicesAvailable: Boolean, - onClickPharmacySearch: () -> Unit, - onClickMapsSearch: () -> Unit, - onClickQuickFilterSearch: (PharmacyUseCaseData.Filter) -> Unit, - onClickFilter: () -> Unit, - onClickFavouritePharmacy: (OverviewPharmacy) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - state = listState, - contentPadding = paddingValues - ) { - item { - SpacerLarge() - } - PharmacySearchButton( - modifier = Modifier - .padding(horizontal = PaddingDefaults.Medium), - onStartSearch = onClickPharmacySearch - ) - if (isGooglePlayServicesAvailable) { - MapsTitle() - MapsTile { onClickMapsSearch() } - } - FilterSection( - filter = filter, - onClickFilter = onClickFilter, - onSelectFilter = onClickQuickFilterSearch - ) - if (pharmacies.isNotEmpty()) { - FavouritePharmacies( - modifier = Modifier - .padding(horizontal = PaddingDefaults.Medium) - .fillMaxWidth(), - pharmacies = pharmacies - ) { - onClickFavouritePharmacy(it) - } - item { - SpacerLarge() - } - } - } -} - -@LightDarkPreview -@Composable -fun PharmacyStartScreenPreview() { - val time = Instant.parse("2022-01-01T00:00:00Z") - PreviewAppTheme { - Column { - PharmacyStartScreenContent( - listState = rememberLazyListState(), - filter = PharmacyUseCaseData.Filter(), - pharmacies = listOf( - OverviewPharmacy( - lastUsed = time, - isFavorite = false, - usageCount = 1, - telematikId = "123456789", - pharmacyName = "Berlin Apotheke", - address = "BerlinStr, 12345 Berlin" - ), - OverviewPharmacy( - lastUsed = time, - isFavorite = true, - usageCount = 1, - telematikId = "123456788", - pharmacyName = "Stuttgart Apotheke", - address = "StuttgartStr, 12345 Stuttgart" - ) - ), - isGooglePlayServicesAvailable = true, - onClickQuickFilterSearch = {}, - onClickFavouritePharmacy = {}, - onClickPharmacySearch = {}, - onClickMapsSearch = {}, - onClickFilter = {}, - onBack = {} - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt deleted file mode 100644 index 4166d689..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.toggleable -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material.icons.rounded.RadioButtonUnchecked -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.selected -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.prescriptionId -import de.gematik.ti.erp.app.prescriptionIds -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.extensions.dateTimeShortText - -@Composable -fun PrescriptionSelection( - orderState: PharmacyOrderController, - showNextButton: Boolean = false, - backIsFinish: Boolean = true, - onFinishSelection: () -> Unit, - onBack: () -> Unit -) { - val prescriptions by orderState.prescriptionsState - val order by orderState.orderState - - var showNoSelectedRxDialog by remember { mutableStateOf(false) } - BackHandler(backIsFinish && order.orders.isEmpty()) { - showNoSelectedRxDialog = true - } - - val onFinishFn = { - if (order.orders.isEmpty()) { - showNoSelectedRxDialog = true - } else { - onFinishSelection() - } - } - - if (showNoSelectedRxDialog) { - AcceptDialog( - header = stringResource(R.string.pharmacy_order_no_selected_prescriptions_title), - info = stringResource(R.string.pharmacy_order_no_selected_prescriptions_desc), - acceptText = stringResource(R.string.ok), - onClickAccept = { - showNoSelectedRxDialog = false - } - ) - } - - val listState = rememberLazyListState() - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen), - topBarTitle = stringResource(R.string.pharmacy_order_select_prescriptions), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = if (backIsFinish) onFinishFn else onBack - ) { - Column(Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier - .weight(1f) - .testTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .semantics { - prescriptionIds = prescriptions.map { it.taskId } - }, - state = listState - ) { - prescriptions.forEach { prescription -> - item(key = "prescription-${prescription.taskId}") { - PrescriptionItem( - modifier = Modifier, - prescription = prescription, - checked = remember(prescription, order) { prescription in order.orders }, - onCheckedChange = { - if (it) { - orderState.onSelectPrescription(prescription) - } else { - orderState.onDeselectPrescription(prescription) - } - } - ) - } - } - } - if (showNextButton) { - NextButton( - onNext = onFinishFn - ) - } - } - } -} - -@Composable -private fun PrescriptionItem( - modifier: Modifier, - prescription: PharmacyUseCaseData.PrescriptionOrder, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - val titlePrepend = stringResource(R.string.pres_details_scanned_medication) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .toggleable( - value = checked, - onValueChange = onCheckedChange, - role = Role.Checkbox - ) - .padding(PaddingDefaults.Medium) - .semantics { - selected = checked - prescriptionId = prescription.taskId - } - ) { - val prescriptionDateTime = remember(prescription) { dateTimeShortText(prescription.timestamp) } - Column(Modifier.weight(1f)) { - Text( - prescription.title - ?: "$titlePrepend ${prescription.index}", - style = AppTheme.typography.body1 - ) - Text( - prescriptionDateTime, - style = AppTheme.typography.body2l - ) - } - SpacerMedium() - Box( - contentAlignment = Alignment.Center - ) { - if (checked) { - Icon( - Icons.Rounded.CheckCircle, - null, - tint = AppTheme.colors.primary600 - ) - } else { - Icon( - Icons.Rounded.RadioButtonUnchecked, - null, - tint = AppTheme.colors.neutral400 - ) - } - } - } -} - -@Composable -private fun NextButton( - onNext: () -> Unit -) { - Surface( - modifier = Modifier.fillMaxWidth(), - elevation = 4.dp - ) { - Column(Modifier.navigationBarsPadding()) { - SpacerMedium() - PrimaryButtonLarge( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .testTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton), - onClick = onNext - ) { - Text(stringResource(R.string.rx_selection_next)) - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt index b160d813..2905d0a9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt @@ -1,40 +1,42 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.ui import android.content.Context +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.ErpServiceErrorState +import de.gematik.ti.erp.app.api.GeneralErrorState import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.RedeemPrescriptionsController -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -fun redeemErrorMessage(context: Context, redeemState: PrescriptionServiceErrorState): String? = - when (redeemState) { - GeneralErrorState.NetworkNotAvailable -> - context.getString(R.string.error_message_network_not_available) +@Requirement( + "O.Plat_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "String resources are used tp show the mapped errors." +) +fun ErpServiceErrorState.redeemErrorMessage(context: Context): String? = + when (this) { + GeneralErrorState.NetworkNotAvailable -> context.getString(R.string.error_message_network_not_available) is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - context.getString(R.string.error_message_server_communication_failed).format(redeemState.code) - GeneralErrorState.FatalTruststoreState -> - context.getString(R.string.error_message_vau_error) - is RedeemPrescriptionsController.State.Error.Unknown -> - context.getString(R.string.redeem_online_error_uploading) - is GeneralErrorState.NoneEnrolled -> - context.getString(R.string.no_auth_enrolled) + context.getString(R.string.error_message_server_communication_failed).format(this.code) + + GeneralErrorState.FatalTruststoreState -> context.getString(R.string.error_message_vau_error) + // is RedeemPrescriptionState.Unknown -> context.getString(R.string.redeem_online_error_uploading) + is GeneralErrorState.NoneEnrolled -> context.getString(R.string.no_auth_enrolled) else -> null } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt deleted file mode 100644 index bcae78c2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import android.media.MediaPlayer -import android.view.SurfaceHolder -import android.view.SurfaceView -import androidx.annotation.RawRes -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.updateLayoutParams -import kotlin.math.max - -/** - * [MediaPlayer] backed video composable. - * - * @param aspectRatioOverwrite Prevents the delayed aspect ratio calculation of the video source. Defaults to `null`. - * @param source Android resource - */ -@Composable -fun VideoContent( - modifier: Modifier = Modifier, - aspectRatioOverwrite: Float? = null, - @RawRes source: Int -) { - val context = LocalContext.current - var aspectRatio by remember(aspectRatioOverwrite) { - mutableStateOf(aspectRatioOverwrite ?: 0f) - } - val player = remember(source) { - MediaPlayer().apply { - setDataSource(context.resources.openRawResourceFd(source)) - setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) - - setOnVideoSizeChangedListener { mp, width, height -> - if (aspectRatioOverwrite == null) { - aspectRatio = width / max(1f, height.toFloat()) - } - } - - isLooping = true - } - } - - val surfaceCallback = remember(source) { - object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - player.prepare() - player.start() - player.setDisplay(holder) - } - - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} - - override fun surfaceDestroyed(holder: SurfaceHolder) { - player.stop() - player.setDisplay(null) - } - } - } - - var size by remember { mutableStateOf(IntSize(1, 1)) } - AndroidView( - factory = { ctx -> - val view = SurfaceView(ctx) - - view.holder.addCallback(surfaceCallback) - - view - }, - modifier = modifier - .then( - // prevent irritating large surfaces on first layout calc - if (aspectRatio == 0f) { - Modifier - } else { - Modifier.aspectRatio(aspectRatio) - } - ) - .onSizeChanged { - size = it - } - ) { - it.updateLayoutParams { - this.height = size.width - this.width = size.height - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/ContactInputFields.kt similarity index 91% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/ContactInputFields.kt index 9be14af9..ad22a7c4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/ContactInputFields.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.pharmacy.ui.model +package de.gematik.ti.erp.app.pharmacy.ui.components import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState @@ -25,9 +25,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.ui.ValidationResult -import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus +import de.gematik.ti.erp.app.redeem.ui.screens.ValidationResult import de.gematik.ti.erp.app.utils.compose.InputField +import de.gematik.ti.erp.app.utils.compose.scrollOnFocus fun LazyListScope.phoneNumberInputField( listState: LazyListState, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoritePharmacyCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoritePharmacyCard.kt index dc9ef4dc..0deecc16 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoritePharmacyCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoritePharmacyCard.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.ui.components @@ -33,7 +33,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,10 +46,9 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults import de.gematik.ti.erp.app.utils.compose.ErezeptText import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import kotlinx.datetime.Instant -@OptIn(ExperimentalMaterial3Api::class) @Composable fun FavoritePharmacyCard( modifier: Modifier = Modifier, @@ -59,7 +57,8 @@ fun FavoritePharmacyCard( ) { Card( modifier = modifier - .clip(RoundedCornerShape(SizeDefaults.double)), + .clip(RoundedCornerShape(SizeDefaults.double)) + .padding(bottom = PaddingDefaults.Medium), colors = CardDefaults.cardColors( containerColor = AppTheme.colors.neutral000 ), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoriteStarButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoriteStarButton.kt new file mode 100644 index 00000000..bc3cce49 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavoriteStarButton.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarBorder +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.shortToast + +@Composable +internal fun FavoriteStarButton( + isMarked: Boolean, + modifier: Modifier = Modifier, + onChange: (Boolean) -> Unit +) { + val color = if (isMarked) { + AppTheme.colors.yellow500 + } else { + AppTheme.colors.primary600 + } + + val icon = if (isMarked) { + Icons.Rounded.Star + } else { + Icons.Rounded.StarBorder + } + + val addedText = stringResource(R.string.pharmacy_detals_added_to_favorites) + val removedText = stringResource(R.string.pharmacy_detalls_removed_from_favorites) + val contentDescription = stringResource(id = R.string.pharmacy_search_favorite_toggle) + val activeDescription = stringResource(id = R.string.pharmacy_search_favorite_toggle_active) + val inactiveDescription = stringResource(id = R.string.pharmacy_search_favorite_toggle_inactive) + val context = LocalContext.current + val haptic = LocalHapticFeedback.current + + TertiaryButton( + modifier = modifier + .size(56.dp) + .semantics { + stateDescription = if (isMarked) activeDescription else inactiveDescription + }, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onChange(!isMarked) + context.shortToast( + when { + !isMarked -> addedText + else -> removedText + } + ) + }, + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + Icon( + icon, + contentDescription = contentDescription, + tint = color + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavouritePharmacies.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavouritePharmacies.kt index ec851d98..96d71a26 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavouritePharmacies.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FavouritePharmacies.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("FunctionName") @@ -32,7 +32,6 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium fun LazyListScope.FavouritePharmacies( modifier: Modifier = Modifier, @@ -46,8 +45,7 @@ fun LazyListScope.FavouritePharmacies( Text( text = stringResource(R.string.pharmacy_my_pharmacies_header), style = AppTheme.typography.subtitle1, - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium), + modifier = Modifier.padding(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Medium), textAlign = TextAlign.Start ) } @@ -58,6 +56,5 @@ fun LazyListScope.FavouritePharmacies( overviewPharmacy = it, onClickPharmacy = onClickPharmacy ) - SpacerMedium() } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButton.kt index 79aa6a42..18f48652 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButton.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButton.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.ui.components @@ -33,7 +33,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerMedium @Composable internal fun FilterButton( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButtonSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButtonSection.kt new file mode 100644 index 00000000..a054dca8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterButtonSection.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Tune +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.Chip +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun FilterButtonSection( + modifier: Modifier = Modifier, + filter: PharmacyUseCaseData.Filter, + rowState: LazyListState = rememberLazyListState(), + onClickChip: (Boolean, FilterType) -> Unit, + onClickFilter: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = PaddingDefaults.Medium) + .then(modifier) + ) { + SpacerMedium() + Row( + modifier = Modifier + .clip(RoundedCornerShape(SizeDefaults.one)) + .clickable { + onClickFilter() + } + .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(SizeDefaults.one)) + .padding(horizontal = PaddingDefaults.Small, vertical = SizeDefaults.threeQuarter), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.Tune, null, Modifier.size(SizeDefaults.double), tint = AppTheme.colors.primary600) + SpacerSmall() + Text( + stringResource(R.string.search_pharmacies_filter), + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600 + ) + } + if (filter.isAnySet()) { + SpacerSmall() + val contentDescription = stringResource(id = R.string.pharmacy_search_active_filter) + LazyRow( + state = rowState, + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + modifier = Modifier.fillMaxWidth() + ) { + if (filter.nearBy) { + item { + Chip( + stringResource(R.string.search_pharmacies_filter_nearby), + modifier = Modifier.semantics { stateDescription = contentDescription }, + closable = true, + checked = false + ) { + onClickChip(it, FilterType.NEARBY) + } + } + } + if (filter.openNow) { + item { + Chip( + stringResource(R.string.search_pharmacies_filter_open_now), + modifier = Modifier.semantics { stateDescription = contentDescription }, + closable = true, + checked = false + ) { + onClickChip(it, FilterType.OPEN_NOW) + } + } + } + if (filter.deliveryService) { + item { + Chip( + stringResource(R.string.search_pharmacies_filter_delivery_service), + modifier = Modifier.semantics { stateDescription = contentDescription }, + closable = true, + checked = false + ) { + onClickChip(it, FilterType.DELIVERY_SERVICE) + } + } + } + if (filter.onlineService) { + item { + Chip( + stringResource(R.string.search_pharmacies_filter_online_service), + modifier = Modifier.semantics { stateDescription = contentDescription }, + closable = true, + checked = false + ) { + onClickChip(it, FilterType.ONLINE_SERVICE) + } + } + } + item { + SpacerMedium() + } + } + } + } +} + +@LightDarkPreview +@Composable +fun FilterButtonSectionPreview() { + PreviewAppTheme { + FilterButtonSection( + filter = PharmacyUseCaseData.Filter( + openNow = true, + deliveryService = true, + onlineService = true, + nearBy = true + ), + onClickChip = { _, _ -> + }, + onClickFilter = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterSection.kt index 1d9034f8..e0b2e02c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterSection.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/FilterSection.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("FunctionName") @@ -35,13 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.pharmacy.ui.model.QuickFilter import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults internal fun LazyListScope.FilterSection( - filter: PharmacyUseCaseData.Filter, - onSelectFilter: (PharmacyUseCaseData.Filter) -> Unit, + onSelectFilter: (QuickFilter) -> Unit, onClickFilter: () -> Unit ) { item { @@ -62,14 +61,7 @@ internal fun LazyListScope.FilterSection( text = stringResource(R.string.search_pharmacies_filter_open_now_and_local), icon = Icons.Outlined.LocationOn, onClick = { - onSelectFilter( - filter.copy( - nearBy = true, - openNow = true, - deliveryService = false, - onlineService = false - ) - ) + onSelectFilter(QuickFilter.OpenNowNearby) } ) FilterButton( @@ -77,14 +69,7 @@ internal fun LazyListScope.FilterSection( text = stringResource(R.string.search_pharmacies_filter_delivery_service), icon = Icons.Outlined.Moped, onClick = { - onSelectFilter( - filter.copy( - nearBy = true, - deliveryService = true, - onlineService = false, - openNow = false - ) - ) + onSelectFilter(QuickFilter.DeliveryNearby) } ) FilterButton( @@ -92,14 +77,7 @@ internal fun LazyListScope.FilterSection( text = stringResource(R.string.search_pharmacies_filter_online_service), icon = Icons.Outlined.LocalShipping, onClick = { - onSelectFilter( - filter.copy( - nearBy = false, - onlineService = true, - deliveryService = false, - openNow = false - ) - ) + onSelectFilter(QuickFilter.Online) } ) FilterButton( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/LocationPermissionDialogs.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/LocationPermissionDialogs.kt new file mode 100644 index 00000000..cf26a316 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/LocationPermissionDialogs.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun LocationPermissionDeniedDialog( + event: ComposableEvent, + dialog: DialogScaffold, + onClick: () -> Unit +) { + event.listen { + dialog.show { + LocationPermissionDeniedDialog( + onClick = { + onClick() + it.dismiss() + } + ) + } + } +} + +@Composable +fun LocationServicesNotAvailableDialog( + event: ComposableEvent, + dialog: DialogScaffold, + onClickDismiss: () -> Unit, + onClickSettings: () -> Unit +) { + event.listen { + dialog.show { + LocationServicesNotAvailableDialog( + onClickDismiss = { + onClickDismiss() + it.dismiss() + }, + onClickSettings = { + onClickSettings() + it.dismiss() + } + ) + } + } +} + +@Composable +private fun LocationPermissionDeniedDialog( + onClick: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.search_pharmacies_location_na_header), + body = stringResource(R.string.search_pharmacies_location_na_header_info), + onDismissRequest = onClick + ) +} + +@Composable +fun LocationServicesNotAvailableDialog( + onClickDismiss: () -> Unit, + onClickSettings: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.search_pharmacies_location_na_header), + bodyText = stringResource(R.string.search_pharmacies_location_na_services), + confirmText = stringResource(R.string.search_pharmacies_location_na_settings), + dismissText = stringResource(R.string.cancel), + onDismissRequest = onClickDismiss, + onConfirmRequest = onClickSettings + ) +} + +@LightDarkPreview +@Composable +fun LocationPermissionDeniedDialogPreview() { + PreviewAppTheme { + LocationPermissionDeniedDialog {} + } +} + +@LightDarkPreview +@Composable +fun LocationServicesNotAvailableDialogPreview() { + PreviewAppTheme { + LocationServicesNotAvailableDialog({}, {}) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsSection.kt new file mode 100644 index 00000000..a3a05089 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsSection.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Suppress("FunctionName") +internal fun LazyListScope.MapsTitle() { + item { + Text( + stringResource(R.string.pharmacy_maps_header), + style = AppTheme.typography.subtitle1, + modifier = Modifier + .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium) + .padding(horizontal = PaddingDefaults.Medium) + ) + } +} + +@Suppress("FunctionName") +internal fun LazyListScope.MapsTile( + previewCoordinates: Coordinates, + previewMap: PharmacyMap, + onClick: () -> Unit +) { + item { + val positionState = remember(previewCoordinates) { + PositionState( + position = previewCoordinates, + zoom = DefaultZoomLevel + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(SizeDefaults.twentythreefold) + .padding(horizontal = PaddingDefaults.Medium) + ) { + previewMap.Map( + modifier = Modifier, + isFullScreen = false, + onClick = onClick, + positionState = positionState, + settings = PharmacySettings.Default, + properties = PharmacyProperties.Default, + contentPaddingValues = PaddingValues(), + onZoomStateChanged = { + // zoom state cannot be changed on preview + }, + content = null + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsTile.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsTile.kt deleted file mode 100644 index 080ad304..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/MapsTile.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("FunctionName") - -package de.gematik.ti.erp.app.pharmacy.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import com.google.android.gms.maps.model.CameraPosition -import com.google.android.gms.maps.model.LatLng -import com.google.maps.android.compose.GoogleMap -import com.google.maps.android.compose.MapUiSettings -import com.google.maps.android.compose.rememberCameraPositionState -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults - -private val Berlin = LatLng(52.51947562977698, 13.404335795642881) -private const val DefaultZoomLevel = 12.2f - -internal fun LazyListScope.MapsTitle() { - item { - Text( - stringResource(R.string.pharmacy_maps_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium) - .padding(horizontal = PaddingDefaults.Medium) - ) - } -} - -internal fun LazyListScope.MapsTile( - onClick: () -> Unit -) { - item { - val cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(Berlin, DefaultZoomLevel) - } - Box( - modifier = Modifier - .fillMaxWidth() - .height(SizeDefaults.twentythreefold) - .padding(horizontal = PaddingDefaults.Medium) - ) { - GoogleMap( - modifier = Modifier - .clip(RoundedCornerShape(SizeDefaults.double)), - onMapClick = { onClick() }, - onMyLocationClick = { onClick() }, - onPOIClick = { onClick() }, - cameraPositionState = cameraPositionState, - uiSettings = remember { switchedOffMapUiSettings } - ) - } - } -} - -private val switchedOffMapUiSettings = MapUiSettings( - compassEnabled = false, - indoorLevelPickerEnabled = false, - mapToolbarEnabled = false, - myLocationButtonEnabled = false, - rotationGesturesEnabled = false, - scrollGesturesEnabled = false, - scrollGesturesEnabledDuringRotateOrZoom = false, - tiltGesturesEnabled = false, - zoomControlsEnabled = false, - zoomGesturesEnabled = false -) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/NoRedeemableOrdersDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/NoRedeemableOrdersDialog.kt new file mode 100644 index 00000000..b74c74fd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/NoRedeemableOrdersDialog.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun NoRedeemableOrdersDialog( + event: ComposableEvent, + dialog: DialogScaffold +) { + event.listen { + dialog.show { + NoRedeemableOrdersDialog( + onClick = { + it.dismiss() + } + ) + } + } +} + +@Composable +private fun NoRedeemableOrdersDialog( + onClick: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(id = R.string.pharmacy_order_no_prescriptions_title), + body = stringResource(id = R.string.pharmacy_order_no_prescriptions_desc), + onDismissRequest = onClick + ) +} + +@LightDarkPreview +@Composable +fun NoRedeemableOrdersDialogPreview() { + PreviewAppTheme { + NoRedeemableOrdersDialog( + onClick = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/OrderSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/OrderSelection.kt new file mode 100644 index 00000000..21a71f86 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/OrderSelection.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.deliveryUrlNotEmpty +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isDeliveryWithoutContactUrls +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isOnlineServiceWithoutContactUrls +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.onlineUrlNotEmpty +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.pickupUrlNotEmpty +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.shortToast + +private const val DISABLED_ALPHA = 0.3f +private const val ENABLED_ALPHA = 1f + +@Requirement( + "A_24579#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Implementation of the order selection for the pharmacy screen." +) +@Composable +internal fun OrderSelection( + pharmacy: Pharmacy, + isDirectRedeemEnabled: Boolean, + onOrderClicked: (Pharmacy, PharmacyScreenData.OrderOption) -> Unit +) { + val directRedeemUrlsNotPresent = pharmacy.directRedeemUrlsNotPresent + + val (pickUpContactAvailable, deliveryContactAvailable, onlineContactAvailable) = + pharmacy.checkRedemptionAndContactAvailabilityForPharmacy(isDirectRedeemEnabled) + + val (pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) = + pharmacy.checkServiceVisibility( + directRedeemUrlsNotPresent = directRedeemUrlsNotPresent, + deliveryServiceAvailable = deliveryContactAvailable, + onlineServiceAvailable = onlineContactAvailable + ) + + val (pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) = + pharmacy.checkServiceAvailability( + directRedeemEnabled = isDirectRedeemEnabled, + pickUpContactAvailable = pickUpContactAvailable, + deliveryContactAvailable = deliveryContactAvailable, + onlineContactAvailable = onlineContactAvailable + ) + + Row( + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxSize(), + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + if (pickUpServiceVisible) { + OrderButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), + isServiceEnabled = pickupServiceEnabled, + text = stringResource(R.string.pharmacy_order_opt_collect_two_lines), + image = painterResource(R.drawable.pharmacy_small), + onClick = { + onOrderClicked( + pharmacy, + PharmacyScreenData.OrderOption.PickupService + ) + } + ) + } + + if (deliveryServiceVisible) { + OrderButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), + isServiceEnabled = deliveryServiceEnabled, + text = stringResource(R.string.pharmacy_order_opt_delivery_two_lines), + image = painterResource(R.drawable.delivery_car_small), + onClick = { + onOrderClicked( + pharmacy, + PharmacyScreenData.OrderOption.CourierDelivery + ) + } + ) + } + + if (onlineServiceVisible) { + OrderButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton), + isServiceEnabled = onlineServiceEnabled, + text = stringResource(R.string.pharmacy_order_opt_mail_two_lines), + image = painterResource(R.drawable.truck_small), + onClick = { onOrderClicked(pharmacy, PharmacyScreenData.OrderOption.MailDelivery) } + ) + } + } +} + +@Composable +private fun RowScope.OrderButton( + modifier: Modifier, + isServiceEnabled: Boolean, + text: String, + image: Painter, + onClick: () -> Unit +) { + val shape = RoundedCornerShape(PaddingDefaults.Medium) + val serviceDisabledText = stringResource(R.string.connect_for_pharmacy_service) + var showToast by remember { mutableStateOf(false) } + + // set the toast to be false on every recomposition + LaunchedEffect(showToast) { + showToast = false + } + + Column( + modifier = modifier + .weight(1f) + .fillMaxSize(1f) + .shadow(elevation = SizeDefaults.half, shape = shape) + .background(AppTheme.colors.neutral025, shape) + .border( + width = SizeDefaults.eighth, + shape = shape, + color = AppTheme.colors.neutral300 + ) + .clip(shape) + .clickable( + role = Role.Button, + onClick = { + when { + isServiceEnabled -> onClick() + else -> showToast = true + } + } + ) + .padding(PaddingDefaults.Medium) + .alpha( + when { + isServiceEnabled -> ENABLED_ALPHA + else -> DISABLED_ALPHA + } + ) + ) { + Image(image, null, modifier = Modifier.align(Alignment.CenterHorizontally)) + SpacerTiny() + Text( + text, + modifier = Modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle2 + ) + } + + AnimatedVisibility(showToast) { + shortToast(serviceDisabledText) + } +} + +private fun Pharmacy.checkRedemptionAndContactAvailabilityForPharmacy( + directRedeemEnabled: Boolean +): Triple { + val pickUpServiceAvailable = directRedeemEnabled && pickupUrlNotEmpty() + + val deliveryServiceAvailable = directRedeemEnabled && deliveryUrlNotEmpty() + + val onlineServiceAvailable = directRedeemEnabled && onlineUrlNotEmpty() + + return Triple(pickUpServiceAvailable, deliveryServiceAvailable, onlineServiceAvailable) +} + +private fun Pharmacy.checkServiceVisibility( + directRedeemUrlsNotPresent: Boolean, + deliveryServiceAvailable: Boolean, + onlineServiceAvailable: Boolean +): Triple { + val pickUpServiceVisible = pickupUrlNotEmpty() || directRedeemUrlsNotPresent + + val deliveryServiceVisible = deliveryServiceAvailable || + deliveryUrlNotEmpty() || + isDeliveryWithoutContactUrls(directRedeemUrlsNotPresent) + + val onlineServiceVisible = onlineServiceAvailable || + onlineUrlNotEmpty() || + isOnlineServiceWithoutContactUrls(directRedeemUrlsNotPresent) + + return Triple(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) +} + +private fun Pharmacy.checkServiceAvailability( + directRedeemEnabled: Boolean, + pickUpContactAvailable: Boolean, + deliveryContactAvailable: Boolean, + onlineContactAvailable: Boolean +): Triple { + val pickupServiceEnabled = pickupUrlNotEmpty() || + pickUpContactAvailable || + !directRedeemEnabled && isPickupService + + val deliveryServiceEnabled = deliveryUrlNotEmpty() || + deliveryContactAvailable || + !directRedeemEnabled && isDeliveryService + + val onlineServiceEnabled = onlineUrlNotEmpty() || + onlineContactAvailable || + !directRedeemEnabled && isOnlineService + + return Triple(pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContact.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContact.kt new file mode 100644 index 00000000..7f9f72ab --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContact.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.OpeningHours +import de.gematik.ti.erp.app.fhir.model.OpeningTime +import de.gematik.ti.erp.app.pharmacy.ui.preview.mockDetailedInfoText +import de.gematik.ti.erp.app.pharmacy.ui.preview.mockOpeningHours +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.ErezeptText +import de.gematik.ti.erp.app.utils.compose.HintTextActionButton +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.dateTimeHHMMFormatter +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.toJavaLocalTime +import java.time.format.TextStyle +import java.util.Locale + +@Composable +internal fun PharmacyContact( + openingHours: OpeningHours?, + phone: String, + mail: String, + url: String, + detailedInfoText: AnnotatedString, + onPhoneClicked: (String) -> Unit, + onMailClicked: (String) -> Unit, + onUrlClicked: (String) -> Unit, + onTextClicked: (Int) -> Unit, + onHintClicked: () -> Unit, + locale: Locale = Locale.getDefault(), + currentDateTime: LocalDateTime +) { + Column { + openingHours?.let { + if (it.isNotEmpty()) { + PharmacyOpeningHoursOverview( + openingHours = it, + locale = locale, + currentDateTime = currentDateTime + ) + } + } + SpacerLarge() + + if (phone.isNotEmpty() || mail.isNotEmpty() || url.isNotEmpty()) { + ErezeptText.Title(text = stringResource(id = R.string.legal_notice_contact_header)) + SpacerMedium() + } + if (phone.isNotEmpty()) { + ContactLabel( + text = phone, + label = stringResource(R.string.pres_detail_organization_label_telephone), + onClick = onPhoneClicked + ) + SpacerMedium() + } + if (mail.isNotEmpty()) { + ContactLabel( + text = mail, + label = stringResource(R.string.pres_detail_organization_label_email), + onClick = onMailClicked + ) + SpacerMedium() + } + if (url.isNotEmpty()) { + ContactLabel( + text = url, + label = stringResource(R.string.pharm_detail_website), + onClick = onUrlClicked + ) + SpacerMedium() + } + SpacerMedium() + DataInfoSection( + modifier = Modifier.align(Alignment.End), + detailedInfoText = detailedInfoText, + onTextClicked = onTextClicked, + onHintClicked = onHintClicked + ) + } +} + +@Composable +private fun ContactLabel( + text: String, + label: String, + onClick: (String) -> Unit +) { + Column( + modifier = Modifier + .clickable { + onClick(text) + } + .fillMaxWidth() + ) { + Text( + text = text, + style = AppTheme.typography.body1, + color = AppTheme.colors.primary600 + ) + SpacerTiny() + Text( + text = label, + style = AppTheme.typography.body2l + ) + } +} + +@Composable +private fun DataInfoSection( + modifier: Modifier, + detailedInfoText: AnnotatedString, + onTextClicked: (Int) -> Unit, + onHintClicked: () -> Unit +) { + ClickableText( + modifier = modifier + .fillMaxWidth(), + text = detailedInfoText, + style = AppTheme.typography.body2l, + onClick = onTextClicked + ) + SpacerSmall() + Row(modifier = modifier) { + HintTextActionButton( + text = stringResource(R.string.pharmacy_detail_data_info_btn), + onClick = onHintClicked + ) + } +} + +@Composable +private fun PharmacyOpeningHoursOverview( + openingHours: OpeningHours, + locale: Locale = Locale.getDefault(), + currentDateTime: LocalDateTime +) { + val todayRelevantDay = currentDateTime.dayOfWeek + val tomorrowRelevantDay = todayRelevantDay.plus(1) + + val sortedOpeningHours = remember { + val daysOfWeek = DayOfWeek.entries.toTypedArray() + val startIndex = todayRelevantDay.ordinal + daysOfWeek.indices + .map { daysOfWeek[(startIndex + it) % daysOfWeek.size] } + .filter { openingHours.containsKey(it) } + .associateWith { openingHours[it] ?: emptyList() } + } + + Column { + Text( + text = stringResource(R.string.pharm_detail_opening_hours), + style = AppTheme.typography.h6 + ) + + SpacerMedium() + + sortedOpeningHours.forEach { (day, hours) -> + val isOpenToday = remember(currentDateTime) { day == todayRelevantDay && hours.isNotEmpty() } + PharmacyDayOpeningHoursDisplay( + day = day, + hours = hours, + isOpenToday = isOpenToday, + currentDateTime = currentDateTime, + todayRelevantDay = todayRelevantDay, + nextRelevantDay = tomorrowRelevantDay, + isNextDayAfterToday = day == tomorrowRelevantDay, + locale = locale + ) + SpacerMedium() + } + } +} + +@Composable +private fun PharmacyDayOpeningHoursDisplay( + day: DayOfWeek, + hours: List, + isOpenToday: Boolean, + currentDateTime: LocalDateTime, + todayRelevantDay: DayOfWeek, + nextRelevantDay: DayOfWeek, + isNextDayAfterToday: Boolean, + locale: Locale +) { + val dayLabel = generateDayLabel(day, todayRelevantDay, nextRelevantDay, isNextDayAfterToday, locale) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = dayLabel, + color = AppTheme.colors.neutral900, + fontWeight = if (isOpenToday) FontWeight.Medium else null + ) + Column(horizontalAlignment = Alignment.End) { + hours.sortedBy { it.openingTime }.forEach { hour -> + val text = "${hour.openingTime?.toHHMM() ?: ""} " + + "- ${hour.closingTime?.toHHMM() ?: ""}" + val isOpenNow = remember(currentDateTime) { + hour.isOpenAt(currentDateTime.time) && isOpenToday + } + Text( + text = text, + color = when { + isOpenNow -> AppTheme.colors.green600 + isOpenToday -> AppTheme.colors.neutral600 + else -> AppTheme.colors.neutral600 + }, + fontWeight = if (isOpenNow || isOpenToday) FontWeight.Medium else null + ) + } + } + } +} + +private fun LocalTime?.toHHMM(): String { + return this?.toJavaLocalTime()?.format(dateTimeHHMMFormatter) ?: "" +} + +@Composable +private fun generateDayLabel( + day: DayOfWeek, + todayRelevantDay: DayOfWeek, + nextRelevantDay: DayOfWeek, + isNextDayAfterToday: Boolean, + locale: Locale +): String { + return when (day) { + todayRelevantDay -> stringResource(id = R.string.pharmacy_day_today) + nextRelevantDay -> if (isNextDayAfterToday) { + stringResource(id = R.string.pharmacy_day_tommorow) + } else { + day.getDisplayName(TextStyle.FULL, locale) + } + + else -> day.getDisplayName(TextStyle.FULL, locale) + } +} + +@Suppress("MagicNumber") +@LightDarkPreview +@Composable +fun PreviewPharmacyContact() { + PreviewAppTheme { + PharmacyContact( + openingHours = mockOpeningHours, + phone = "123-456-7890", + mail = "pharmacy@example.com", + url = "www.examplepharmacy.com", + locale = Locale.GERMANY, + detailedInfoText = mockDetailedInfoText, + onPhoneClicked = {}, + onMailClicked = {}, + onUrlClicked = {}, + onTextClicked = {}, + onHintClicked = {}, + currentDateTime = LocalDateTime(2024, 7, 31, 10, 0) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContactSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContactSelection.kt new file mode 100644 index 00000000..7d79c6c7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyContactSelection.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MailOutline +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material.icons.outlined.PinDrop +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.ui.preview.mockPharmacy +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.BigFontDarkPreview +import de.gematik.ti.erp.app.utils.compose.DarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.gotoCoordinates + +@Composable +fun PharmacyContactSelection( + pharmacy: Pharmacy, + onPhoneClicked: (String) -> Unit, + onMailClicked: (String) -> Unit +) { + val context = LocalContext.current + Row( + modifier = Modifier + .height(IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + PharmacyContactButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), + text = stringResource(R.string.pharmacy_contact_map_two_lines), + icon = Icons.Outlined.PinDrop, + + onClick = { + pharmacy.coordinates?.let { context.gotoCoordinates(it) } + } + ) + PharmacyContactButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), + text = stringResource(R.string.pharmacy_contact_phone_two_lines), + icon = Icons.Outlined.Phone, + onClick = { + onPhoneClicked(pharmacy.contacts.phone) + } + ) + PharmacyContactButton( + modifier = Modifier + .testTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton), + text = stringResource(R.string.pharmacy_contact_email_two_lines), + icon = Icons.Outlined.MailOutline, + onClick = { + onMailClicked(pharmacy.contacts.mail) + } + ) + } +} + +@Composable +private fun RowScope.PharmacyContactButton( + modifier: Modifier, + text: String, + icon: ImageVector? = null, + image: Painter? = null, + onClick: () -> Unit +) { + val shape = RoundedCornerShape(PaddingDefaults.Medium) + Column( + modifier = modifier + .weight(1f) + .fillMaxSize(1f) + .shadow(elevation = SizeDefaults.half, shape = shape) + .background(AppTheme.colors.neutral025, shape) + .border( + width = SizeDefaults.eighth, + shape = shape, + color = AppTheme.colors.neutral300 + ) + .clip(shape) + .clickable( + role = Role.Button, + onClick = { onClick() } + ) + .padding(PaddingDefaults.ShortMedium) + + ) { + if (icon != null) { + Icon( + icon, + null, + tint = AppTheme.colors.primary600, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(SizeDefaults.fourfold) + ) + } + if (image != null) { + Image( + image, + null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(SizeDefaults.fourfold) + ) + } + SpacerTiny() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text, + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle2 + ) + } + } +} + +@BigFontDarkPreview +@Composable +fun PreviewContactSelectionBigFont() { + PreviewAppTheme { + PharmacyContactSelection( + pharmacy = mockPharmacy, + onPhoneClicked = {}, + onMailClicked = {} + ) + } +} + +@DarkPreview +@Composable +fun PreviewContactSelection() { + PreviewAppTheme { + PharmacyContactSelection( + pharmacy = mockPharmacy, + onPhoneClicked = {}, + onMailClicked = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsComponent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsComponent.kt new file mode 100644 index 00000000..b9cc1831 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsComponent.kt @@ -0,0 +1,358 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRouteBackStackEntryArguments +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyDetailsController +import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyPortalText +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacyPreviewData +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacyPreviewParameterProvider +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.providePhoneIntent +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.gotoCoordinates +import de.gematik.ti.erp.app.utils.extensions.openEmailClient +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.util.Locale + +enum class ScreenType { + ForPharmacy, + ForMessage, +} + +@Composable +fun PharmacyDetailsComponent( + navController: NavController, + navBackStackEntry: NavBackStackEntry, + graphController: PharmacyGraphController? = null, + screenType: ScreenType +) { + val keyboardController = LocalSoftwareKeyboardController.current + val noRedeemableTaskDialogEvent = ComposableEvent() + + NoRedeemableOrdersDialog( + event = noRedeemableTaskDialogEvent, + dialog = LocalDialog.current + ) + // this can be opened while the keyboard is open on the list screen, this forces the keyboard to close + LaunchedEffect(Unit) { keyboardController?.hide() } + + PharmacyRouteBackStackEntryArguments(navBackStackEntry).getPharmacy()?.let { pharmacy -> + + val faqUri: String = stringResource(R.string.pharmacy_detail_data_info_faqs_uri) + + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + val urlText = PharmacyPortalText().urlText() + val controller = rememberPharmacyDetailsController() + + LaunchedEffect(pharmacy) { + controller.isPharmacyFavorite(pharmacy) + } + + val isMarkedAsFavorite by controller.isPharmacyFavoriteState + val isDirectRedeemEnabled by ( + graphController?.isDirectRedeemEnabled() + ?: remember { mutableStateOf(false) } + ) + val hasRedeemableOrders by ( + graphController?.hasRedeemableOrders() + ?: remember { mutableStateOf(false) } + ) + + BasePharmacyDetailsContent( + pharmacy = pharmacy, + clickableText = urlText, + isMarkedAsFavorite = isMarkedAsFavorite, + isDirectRedeemEnabled = isDirectRedeemEnabled, + onChangeFavoriteState = { + controller.changePharmacyAsFavorite(pharmacy, it) + }, + onClickOrder = { selectedPharmacy, orderOption -> + if (!hasRedeemableOrders) { + noRedeemableTaskDialogEvent.trigger() + } else { + navController.navigate( + RedeemRoutes.RedeemOrderOverviewScreen.path( + pharmacy = selectedPharmacy, + orderOption = orderOption, + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + } + }, + openExternalMap = { + context.gotoCoordinates(it) + }, + onClickPhone = { + context.handleIntent(providePhoneIntent(it)) + }, + onClickMail = { emailAddress -> + context.openEmailClient(emailAddress) + }, + onClickUrl = { + uriHandler.openUriWhenValid(it) + }, + onClickWebsite = { + urlText.getStringAnnotations("URL", it, it + 1).firstOrNull()?.let { annotation -> + uriHandler.openUriWhenValid(annotation.item) + } + }, + onClickHint = { + uriHandler.openUriWhenValid(faqUri) + }, + screenType = screenType, + locale = Locale.getDefault() + ) + } ?: run { + ErrorScreenComponent() + } +} + +@Composable +private fun BasePharmacyDetailsContent( + pharmacy: PharmacyUseCaseData.Pharmacy, + clickableText: AnnotatedString, + isMarkedAsFavorite: Boolean, + isDirectRedeemEnabled: Boolean, + screenType: ScreenType, + locale: Locale = Locale.getDefault(), + currentDateTime: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()), + onClickOrder: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, + openExternalMap: (Coordinates) -> Unit, + onChangeFavoriteState: (Boolean) -> Unit, + onClickPhone: (String) -> Unit, + onClickMail: (String) -> Unit, + onClickUrl: (String) -> Unit, + onClickWebsite: (Int) -> Unit, + onClickHint: () -> Unit +) { + Scaffold( + modifier = Modifier + .padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + .fillMaxWidth() + .wrapContentHeight() + .testTag(TestTag.PharmacySearch.OrderOptions.Content), + containerColor = AppTheme.colors.neutral000 + + ) { innerPadding -> + LazyColumn( + modifier = Modifier + .padding( + bottom = innerPadding.calculateBottomPadding() + ) + + ) { + item { + SpacerLarge() + } + item { + Row { + Column( + modifier = Modifier + .weight(1f) + .clickable( + role = Role.Button, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + pharmacy.coordinates?.let { openExternalMap(it) } + } + ) { + Text( + text = pharmacy.name, + style = AppTheme.typography.h6, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + SpacerTiny() + Text( + text = pharmacy.singleLineAddress(), + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600, + textDecoration = TextDecoration.Underline, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + SpacerMedium() + if (screenType == ScreenType.ForPharmacy) { + FavoriteStarButton( + isMarked = isMarkedAsFavorite, + modifier = Modifier, + onChange = { + onChangeFavoriteState(it) + } + ) + } + } + SpacerXXLarge() + } + item { + if (screenType == ScreenType.ForPharmacy) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + OrderSelection( + pharmacy = pharmacy, + isDirectRedeemEnabled = isDirectRedeemEnabled, + onOrderClicked = onClickOrder + ) + } + + SpacerXXLarge() + } else if (screenType == ScreenType.ForMessage) { + PharmacyContactSelection( + pharmacy = pharmacy, + onPhoneClicked = onClickPhone, + onMailClicked = onClickMail + ) + SpacerXXLarge() + } + } + item { + PharmacyContact( + openingHours = pharmacy.openingHours, + phone = pharmacy.contacts.phone, + mail = pharmacy.contacts.mail, + url = pharmacy.contacts.url, + detailedInfoText = clickableText, + onPhoneClicked = onClickPhone, + onMailClicked = onClickMail, + onUrlClicked = onClickUrl, + onTextClicked = onClickWebsite, + onHintClicked = onClickHint, + locale = locale, + currentDateTime = currentDateTime + ) + } + item { + SpacerMedium() + } + } + } +} + +@Suppress("MagicNumber") +@LightDarkPreview +@Composable +fun PharmacyDetailsScreenFromPharmacyPreview( + @PreviewParameter( + PharmacyPreviewParameterProvider::class + ) pharmacy: PharmacyUseCaseData.Pharmacy? +) { + PreviewAppTheme { + BasePharmacyDetailsContent( + openExternalMap = {}, + isMarkedAsFavorite = true, + isDirectRedeemEnabled = false, + onChangeFavoriteState = {}, + pharmacy = pharmacy ?: PharmacyPreviewData.LOCAL_PICKUP_ONLY_DATA, + clickableText = PharmacyPortalText().urlText(), + onClickPhone = {}, + onClickMail = {}, + onClickUrl = {}, + onClickHint = {}, + onClickWebsite = {}, + onClickOrder = { _, _ -> }, + screenType = ScreenType.ForPharmacy, + locale = Locale.GERMAN, + currentDateTime = LocalDateTime(2024, 7, 31, 10, 0) + + ) + } +} + +@Suppress("MagicNumber") +@LightDarkPreview +@Composable +fun PharmacyDetailsScreenFromMessagePreview() { + PreviewAppTheme { + BasePharmacyDetailsContent( + openExternalMap = {}, + isMarkedAsFavorite = true, + onChangeFavoriteState = {}, + pharmacy = PharmacyPreviewData.ALL_PRESENT_DATA, + clickableText = PharmacyPortalText().urlText(), + onClickPhone = {}, + onClickMail = {}, + onClickUrl = {}, + onClickHint = {}, + onClickWebsite = {}, + isDirectRedeemEnabled = true, + onClickOrder = { _, _ -> }, + screenType = ScreenType.ForMessage, + locale = Locale.GERMAN, + currentDateTime = LocalDateTime(2024, 7, 31, 10, 0) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMap.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMap.kt new file mode 100644 index 00000000..f2ff701d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMap.kt @@ -0,0 +1,291 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MapStyleOptions +import com.google.maps.android.compose.CameraPositionState +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapType +import com.google.maps.android.compose.MapUiSettings +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import kotlinx.coroutines.flow.MutableStateFlow +import java.util.Calendar + +internal const val DefaultZoomLevel = 10f + +/** + * This class is a wrapper for the GoogleMap composable. + * It is used to abstract the GoogleMap composable and make it easier to test the PharmacyStartScreen. + * It also provides a way to change the implementation of the GoogleMap + */ +interface PharmacyMap { + @Composable + fun Map( + modifier: Modifier, + isFullScreen: Boolean, + positionState: PositionState, + settings: PharmacySettings, + properties: PharmacyProperties, + contentPaddingValues: PaddingValues, + onZoomStateChanged: (Float) -> Unit, + onClick: (() -> Unit)?, + content: (@Composable () -> Unit)? + ) +} + +data class PositionState(val position: Coordinates, val zoom: Float) + +data class PharmacyProperties( + val isBuildingEnabled: Boolean = true, + val isIndoorEnabled: Boolean = false, + val isMyLocationEnabled: Boolean = false, + val isTrafficEnabled: Boolean = false, + val mapType: MapType = MapType.NORMAL, + val maxZoomPreference: Float = 21.0f, + val minZoomPreference: Float = 3.0f +) { + companion object { + val Default = PharmacyProperties() + } +} + +data class PharmacySettings( + val compassEnabled: Boolean = false, + val indoorLevelPickerEnabled: Boolean = false, + val mapToolbarEnabled: Boolean = false, + val myLocationButtonEnabled: Boolean = false, + val rotationGesturesEnabled: Boolean = false, + val scrollGesturesEnabled: Boolean = false, + val scrollGesturesEnabledDuringRotateOrZoom: Boolean = false, + val tiltGesturesEnabled: Boolean = false, + val zoomControlsEnabled: Boolean = false, + val zoomGesturesEnabled: Boolean = false +) { + companion object { + val Default = PharmacySettings() + val FullScreen = PharmacySettings().copy( + zoomControlsEnabled = false, + zoomGesturesEnabled = true, + scrollGesturesEnabled = true, + scrollGesturesEnabledDuringRotateOrZoom = true + ) + } +} + +// This is a mock implementation of the PharmacyMap for previews +class MockMap : PharmacyMap { + @Composable + override fun Map( + modifier: Modifier, + isFullScreen: Boolean, + positionState: PositionState, + settings: PharmacySettings, + properties: PharmacyProperties, + contentPaddingValues: PaddingValues, + onZoomStateChanged: (Float) -> Unit, + onClick: (() -> Unit)?, + content: (@Composable () -> Unit)? + ) { + Surface( + modifier = modifier.fillMaxSize(), + border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.green600), + color = AppTheme.colors.neutral600 + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + color = AppTheme.colors.neutral100, + text = "A map will be shown here" + ) + } + } + } +} + +class GooglePharmacyMap : PharmacyMap { + + private val cameraPositionStateFlow = MutableStateFlow(null) + + val cameraPositionState + @Composable + get() = cameraPositionStateFlow.collectAsStateWithLifecycle() + + @Composable + private fun rememberCameraPositionState( + positionState: PositionState + ) = remember(positionState) { + val state = CameraPositionState( + position = CameraPosition.fromLatLngZoom(positionState.position.toLatLng(), positionState.zoom) + ) + cameraPositionStateFlow.value = state + state + } + + @Composable + override fun Map( + modifier: Modifier, + isFullScreen: Boolean, + positionState: PositionState, + settings: PharmacySettings, + properties: PharmacyProperties, + contentPaddingValues: PaddingValues, + onZoomStateChanged: (Float) -> Unit, + onClick: (() -> Unit)?, + content: (@Composable () -> Unit)? + ) { + val context = LocalContext.current + val cameraPositionState = rememberCameraPositionState(positionState = positionState) + + // Remember the last zoom level to detect changes + var lastZoomLevel by remember(cameraPositionState.position.zoom) { + mutableFloatStateOf(cameraPositionState.position.zoom) + } + + LaunchedEffect(lastZoomLevel) { + if (cameraPositionState.position.zoom != lastZoomLevel) { + // Zoom level has changed due to user interaction + lastZoomLevel = cameraPositionState.position.zoom + onZoomStateChanged(lastZoomLevel) + } + } + + val mapStyleOptions = when { + // isTodayEaster() -> MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_easter_style) + // isTodayChristmas() -> MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_christmas_style) + isSystemInDarkTheme() -> MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_dark_style) + else -> null + } + + val mapProperties = remember(properties) { + MapProperties( + isBuildingEnabled = properties.isBuildingEnabled, + isIndoorEnabled = properties.isIndoorEnabled, + isMyLocationEnabled = properties.isMyLocationEnabled, + isTrafficEnabled = properties.isTrafficEnabled, + latLngBoundsForCameraTarget = null, + mapStyleOptions = mapStyleOptions, + mapType = properties.mapType, + maxZoomPreference = properties.maxZoomPreference, + minZoomPreference = properties.minZoomPreference + ) + } + + val mapSettings = remember(settings) { + MapUiSettings( + compassEnabled = settings.compassEnabled, + indoorLevelPickerEnabled = settings.indoorLevelPickerEnabled, + mapToolbarEnabled = settings.mapToolbarEnabled, + myLocationButtonEnabled = settings.myLocationButtonEnabled, + rotationGesturesEnabled = settings.rotationGesturesEnabled, + scrollGesturesEnabled = settings.scrollGesturesEnabled, + scrollGesturesEnabledDuringRotateOrZoom = settings.scrollGesturesEnabledDuringRotateOrZoom, + tiltGesturesEnabled = settings.tiltGesturesEnabled, + zoomControlsEnabled = settings.zoomControlsEnabled, + zoomGesturesEnabled = settings.zoomGesturesEnabled + ) + } + + GoogleMap( + modifier = modifier.let { + when { + isFullScreen -> it + else -> it.clip(RoundedCornerShape(SizeDefaults.double)) + } + }, + onMapClick = { onClick?.invoke() }, + onMyLocationClick = { onClick?.invoke() }, + onPOIClick = { onClick?.invoke() }, + cameraPositionState = cameraPositionState, + uiSettings = mapSettings, + properties = mapProperties, + contentPadding = contentPaddingValues, + content = { + content?.invoke() + } + ) + } + + companion object { + fun Coordinates.toLatLng() = LatLng(latitude, longitude) + + @Suppress("MagicNumber", "UnusedPrivateMember") + private fun isTodayEaster(): Boolean { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + + val a = year % 19 + val b = year / 100 + val c = year % 100 + val d = b / 4 + val e = b % 4 + val f = (b + 8) / 25 + val g = (b - f + 1) / 3 + val h = (19 * a + b - d - g + 15) % 30 + val i = c / 4 + val k = c % 4 + val l = (32 + 2 * e + 2 * i - h - k) % 7 + val m = (a + 11 * h + 22 * l) / 451 + val month = (h + l - 7 * m + 114) / 31 + val day = ((h + l - 7 * m + 114) % 31) + 1 + + val todayMonth = calendar.get(Calendar.MONTH) + 1 // Note: +1 because Calendar.MONTH is zero-based + val todayDay = calendar.get(Calendar.DAY_OF_MONTH) + + return todayMonth == month && todayDay == day + } + + @Suppress("MagicNumber", "UnusedPrivateMember") + private fun isTodayChristmas(): Boolean { + val calendar = Calendar.getInstance() + // Calendar.MONTH is zero-based, so December is 11 + val todayMonth = calendar.get(Calendar.MONTH) + val todayDay = calendar.get(Calendar.DAY_OF_MONTH) + + // Check if it's December 25th + return todayMonth == Calendar.DECEMBER && todayDay == 25 + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapButton.kt new file mode 100644 index 00000000..e53cc80c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapButton.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("FunctionName") + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Map +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.features.R + +@Composable +internal fun PharmacyMapButton(onClick: () -> Unit) { + val contentDescription = stringResource(id = R.string.google_maps_button) + PrimaryButton( + shape = RoundedCornerShape(SizeDefaults.double), + colors = ButtonDefaults.buttonColors( + backgroundColor = AppTheme.colors.neutral050, + contentColor = AppTheme.colors.primary600 + ), + elevation = ButtonDefaults.elevation(), + modifier = Modifier.size(SizeDefaults.sevenfold), + onClick = onClick + ) { + Icon( + Icons.Rounded.Map, + contentDescription = contentDescription + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapsOverlay.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapsOverlay.kt new file mode 100644 index 00000000..1196c394 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyMapsOverlay.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Tune +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.MyLocation +import androidx.compose.material.icons.rounded.Search +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun BoxScope.pharmacyMapsOverlay( + showSearchButton: Boolean, + isLoading: Boolean, + onSearch: (Boolean) -> Unit, + onClickFilter: () -> Unit, + onBack: () -> Unit +) { + Row( + Modifier + .fillMaxWidth() + .padding( + top = PaddingDefaults.Medium, + start = PaddingDefaults.Small, + end = PaddingDefaults.Medium + ) + .systemBarsPadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + IconButton( + onClick = onBack + ) { + Box( + Modifier + .size(SizeDefaults.fourfold) + .shadow(SizeDefaults.quarter, CircleShape) + .background(AppTheme.colors.neutral100, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Rounded.Close, + contentDescription = null, + tint = AppTheme.colors.primary600, + modifier = Modifier.size(SizeDefaults.triple) + ) + } + } + + IconButton( + modifier = Modifier + .size(SizeDefaults.sixfold) + .shadow(SizeDefaults.quarter, CircleShape) + .border(SizeDefaults.eighth, AppTheme.colors.neutral300, CircleShape) + .background(AppTheme.colors.neutral100, CircleShape), + onClick = onClickFilter + ) { + Icon( + Icons.Outlined.Tune, + contentDescription = null, + tint = AppTheme.colors.primary600, + modifier = Modifier.size(SizeDefaults.triple) + ) + } + } + + Column( + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .systemBarsPadding() + ) { + IconButton( + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = PaddingDefaults.Medium) + .padding(bottom = SizeDefaults.tenfold) + .size(SizeDefaults.sevenfold) + .shadow(SizeDefaults.quarter, CircleShape) + .border(SizeDefaults.eighth, AppTheme.colors.neutral300, CircleShape) + .background(AppTheme.colors.neutral100, CircleShape), + onClick = { + onSearch(true) + } + ) { + Crossfade( + targetState = isLoading, + label = "isLoading" + ) { isLoadingState -> + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when { + isLoadingState -> CircularProgressIndicator(strokeWidth = SizeDefaults.half) + + else -> Icon( + Icons.Rounded.MyLocation, + contentDescription = null, + tint = AppTheme.colors.primary600, + modifier = Modifier.size(SizeDefaults.triple) + ) + } + } + } + } + + AnimatedVisibility( + modifier = Modifier + .align(Alignment.CenterHorizontally), + visible = showSearchButton, + enter = fadeIn() + expandVertically(expandFrom = Alignment.Top) + slideInVertically(), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top) + slideOutVertically() + ) { + PrimaryButtonSmall( + onClick = { + onSearch(false) + }, + modifier = Modifier + .padding(bottom = PaddingDefaults.Large) + ) { + Icon(Icons.Rounded.Search, null) + SpacerSmall() + Text(stringResource(R.string.pharmacy_maps_search_here_button)) + } + } + } +} + +@LightDarkPreview +@Composable +internal fun PharmacyMapsOverlaySearchPreview() { + PreviewAppTheme { + Box(Modifier.fillMaxSize()) { + pharmacyMapsOverlay( + showSearchButton = true, + isLoading = false, + onSearch = {}, + onClickFilter = {}, + onBack = {} + ) + } + } +} + +@LightDarkPreview +@Composable +internal fun PharmacyMapsOverlayLoadingPreview() { + PreviewAppTheme { + Box(Modifier.fillMaxSize()) { + pharmacyMapsOverlay( + showSearchButton = false, + isLoading = true, + onSearch = {}, + onClickFilter = {}, + onBack = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyResultCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyResultCard.kt new file mode 100644 index 00000000..144403fd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyResultCard.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.OpeningHours +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.fhir.model.PharmacyService +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyImagePlaceholder +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.text.DecimalFormat + +private const val OneKilometerInMeter = 1000 + +internal fun formattedDistance(distanceInMeters: Double): String { + val f = DecimalFormat() + return if (distanceInMeters < OneKilometerInMeter) { + f.maximumFractionDigits = 0 + f.format(distanceInMeters).toString() + " m" + } else { + f.maximumFractionDigits = 1 + f.format(distanceInMeters / OneKilometerInMeter).toString() + " km" + } +} + +@Composable +internal fun PharmacyResultCard( + modifier: Modifier, + pharmacy: Pharmacy, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .clickable(onClick = onClick) + .then(modifier), + verticalAlignment = Alignment.CenterVertically + ) { + val distanceTxt = pharmacy.distance?.let { distance -> + formattedDistance(distance) + } + + PharmacyImagePlaceholder(Modifier) + SpacerMedium() + Column(modifier = Modifier.weight(1f)) { + Text( + text = pharmacy.name, + color = AppTheme.colors.neutral999, + style = AppTheme.typography.subtitle1 + ) + + Text( + text = pharmacy.singleLineAddress(), + color = AppTheme.colors.neutral600, + style = AppTheme.typography.body2l, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + + val pharmacyLocalServices = + pharmacy.provides.find { it is PharmacyService.LocalPharmacyService } as PharmacyService + .LocalPharmacyService + val now = + remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } + + if (pharmacyLocalServices.isOpenAt(now)) { + val text = if (pharmacyLocalServices.isAllDayOpen(now.dayOfWeek)) { + stringResource(R.string.search_pharmacy_continuous_open) + } else { + stringResource( + R.string.search_pharmacy_open_until, + requireNotNull(pharmacyLocalServices.openUntil(now)).toString() + ) + } + Text( + text, + style = AppTheme.typography.subtitle2l, + color = AppTheme.colors.green600 + ) + } else { + val text = + pharmacyLocalServices.opensAt(now)?.let { + stringResource( + R.string.search_pharmacy_opens_at, + it.toString() + ) + } + if (text != null) { + Text( + text, + style = AppTheme.typography.subtitle2l, + color = AppTheme.colors.yellow600 + ) + } + } + } + + SpacerMedium() + + if (distanceTxt != null) { + Text( + distanceTxt, + style = AppTheme.typography.body2l, + modifier = Modifier + .align(Alignment.CenterVertically), + textAlign = TextAlign.End + ) + } + Icon( + Icons.Rounded.KeyboardArrowRight, + null, + tint = AppTheme.colors.neutral400, + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterVertically) + ) + } +} + +@LightDarkPreview +@Composable +internal fun PharmacyResultCardPreview() { + AppTheme { + PharmacyResultCard( + modifier = Modifier, + pharmacy = Pharmacy( + id = "pharmacy-id", + name = "2Königen-Aptheke", + address = "Ostwall 97, 47798 Krefeld", + coordinates = null, + distance = null, + contacts = PharmacyContacts( + phone = "12345678", + mail = "pharmacy@mail.com", + url = "https://pharmacy.com", + pickUpUrl = "https://pharmacy.pickup.com/code123", + deliveryUrl = "https://pharmacy.delivery.com/code123", + onlineServiceUrl = "https://pharmacy.online.com/code123" + ), + provides = listOf( + PharmacyService.DeliveryPharmacyService( + name = "delivery-service", + openingHours = OpeningHours(openingTime = mapOf()) + ), + PharmacyService.OnlinePharmacyService(name = "online-service"), + PharmacyService.PickUpPharmacyService(name = "pickup-service"), + PharmacyService.LocalPharmacyService( + name = "local-service", + openingHours = OpeningHours(openingTime = mapOf()) + ) + ), + openingHours = OpeningHours(openingTime = mapOf()), + telematikId = "telematikId" + ), + onClick = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchButton.kt index 78d7bd57..dc7dbc5b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchButton.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchButton.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("FunctionName") @@ -30,6 +30,7 @@ import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Search +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource @@ -39,7 +40,7 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerSmall internal fun LazyListScope.PharmacySearchButton( modifier: Modifier = Modifier, @@ -51,7 +52,8 @@ internal fun LazyListScope.PharmacySearchButton( .clip(RoundedCornerShape(SizeDefaults.double)) .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(SizeDefaults.double)) .clickable(role = Role.Button) { onStartSearch() } - .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.ShortMedium) + .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.ShortMedium), + verticalAlignment = Alignment.CenterVertically ) { Icon( Icons.Rounded.Search, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchErrorHint.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchErrorHint.kt new file mode 100644 index 00000000..1c90c824 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchErrorHint.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +internal fun PharmacySearchErrorHint( + modifier: Modifier = Modifier, + title: String, + subtitle: String, + action: String? = null, + onClickAction: (() -> Unit)? = null +) { + Box( + modifier = modifier + ) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(PaddingDefaults.Medium), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + title, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + Text( + subtitle, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + if (action != null && onClickAction != null) { + TextButton(onClick = onClickAction) { + Text(action) + } + } + } + } +} + +@LightDarkPreview +@Composable +fun PharmacySearchErrorHintPreview() { + PreviewAppTheme { + PharmacySearchErrorHint( + title = "Item has some error", + subtitle = "Subtitle saying that the item has some error", + action = "Retry", + onClickAction = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchLoading.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchLoading.kt new file mode 100644 index 00000000..01cc5857 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacySearchLoading.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.valentinilk.shimmer.shimmer +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.LimitedTextShimmer +import de.gematik.ti.erp.app.utils.compose.RowTextShimmer +import de.gematik.ti.erp.app.utils.compose.SquareShapeShimmer +import de.gematik.ti.erp.app.utils.compose.TinyTextShimmer +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("FunctionName") +internal fun LazyListScope.PharmacySearchLoading( + modifier: Modifier = Modifier +) { + item { + Row( + modifier = modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium) + .shimmer(), + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + verticalAlignment = Alignment.Top + ) { + SquareShapeShimmer( + size = SizeDefaults.eightfold + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Tiny) + ) { + RowTextShimmer() + LimitedTextShimmer() + TinyTextShimmer() + } + } + } +} + +@Suppress("FunctionName") +internal fun LazyListScope.PharmacyFullScreenSearchLoading() { + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() + PharmacySearchLoading() +} + +@LightDarkPreview +@Composable +fun PharmacySearchLoadingPreview() { + PreviewAppTheme { + LazyColumn { + PharmacyFullScreenSearchLoading() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VanishingTopBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VanishingTopBar.kt similarity index 84% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VanishingTopBar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VanishingTopBar.kt index 8f3cd8f5..2df75822 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VanishingTopBar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VanishingTopBar.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.pharmacy.ui +package de.gematik.ti.erp.app.pharmacy.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState @@ -48,6 +48,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -56,7 +57,9 @@ import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerMedium + +val TopBarColor = Color(0xffd6e9fb) // TODO: This is causing the crashes in the store, replaced for now @Suppress("UnusedPrivateMember") diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VideoContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VideoContent.kt new file mode 100644 index 00000000..9ddb8815 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/VideoContent.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import android.media.MediaPlayer +import android.view.SurfaceHolder +import android.view.SurfaceView +import androidx.annotation.RawRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.updateLayoutParams +import kotlin.math.max + +/** + * [MediaPlayer] backed video composable. + * + * @param aspectRatioOverwrite Prevents the delayed aspect ratio calculation of the video source. Defaults to `null`. + * @param source Android resource + */ +@Composable +fun VideoContent( + modifier: Modifier = Modifier, + aspectRatioOverwrite: Float? = null, + @RawRes source: Int +) { + val context = LocalContext.current + var aspectRatio by remember(aspectRatioOverwrite) { + mutableStateOf(aspectRatioOverwrite ?: 0f) + } + val player = remember(source) { + MediaPlayer().apply { + setDataSource(context.resources.openRawResourceFd(source)) + setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING) + + setOnVideoSizeChangedListener { mp, width, height -> + if (aspectRatioOverwrite == null) { + aspectRatio = width / max(1f, height.toFloat()) + } + } + + isLooping = true + } + } + + val surfaceCallback = remember(source) { + object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + player.prepare() + player.start() + player.setDisplay(holder) + } + + @Suppress("EmptyFunctionBlock") + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} + + override fun surfaceDestroyed(holder: SurfaceHolder) { + player.stop() + player.setDisplay(null) + } + } + } + + var size by remember { mutableStateOf(IntSize(1, 1)) } + Box { + AndroidView( + factory = { ctx -> + val view = SurfaceView(ctx) + + view.holder.addCallback(surfaceCallback) + + view + }, + modifier = modifier + .then( + // prevent irritating large surfaces on first layout calc + if (aspectRatio == 0f) { + Modifier + } else { + Modifier.aspectRatio(aspectRatio) + } + ) + .onSizeChanged { + size = it + } + ) { + it.updateLayoutParams { + this.height = size.width + this.width = size.height + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/MapContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/MapContent.kt new file mode 100644 index 00000000..76e956c5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/MapContent.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.model + +import android.content.ComponentName +import android.content.Intent + +data class MapContent( + val component: ComponentName?, + val mapIntent: Intent +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt deleted file mode 100644 index cce689f9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui.model - -import de.gematik.ti.erp.app.analytics.PopUpName -import de.gematik.ti.erp.app.navigation.Routes - -object PharmacyNavigationScreens { - object StartSearch : Routes("pharmacySearch") - object List : Routes("pharmacySearch_detail") - object Maps : Routes("pharmacySearch_map") - object OrderOverview : Routes("redeem_viaTI") // TODO change when redeem_viaAVS is available - object EditShippingContact : Routes("redeem_editContactInformation") - object PrescriptionSelection : Routes("redeem_prescriptionChooseSubset") -} - -object PharmacySearchPopUpNames { - object PharmacySelected : PopUpName("pharmacySearch_selectedPharmacy") - object FilterSelected : PopUpName("pharmacySearch_filter") -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyMapUiParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyMapUiParameter.kt new file mode 100644 index 00000000..a9e830d5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyMapUiParameter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.model + +data class PharmacyMapUiParameter( + val isLoading: Boolean = false, + val isMapLocationEnabled: Boolean = false, + val mapZoomState: Float = 0f, + val radiusFromSearch: Int = 0, + val shouldShowSearchButton: Boolean = false +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyPortalText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyPortalText.kt new file mode 100644 index 00000000..bb6711e4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyPortalText.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.model + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme + +data class PharmacyPortalText( + @StringRes private val portalTextId: Int = R.string.pharmacy_detail_data_info_domain, + @StringRes private val portalUriId: Int = R.string.pharmacy_detail_pharmacy_portal_uri, + @StringRes private val infoTextId: Int = R.string.pharmacy_detail_data_info +) { + private val portalText: String + @Composable + get() = stringResource(portalTextId) + + private val infoText: String + @Composable + get() = stringResource(infoTextId) + + private val portalUri: String + @Composable + get() = stringResource(portalUriId) + + private val start + @Composable + get() = infoText.indexOf(portalText) + + private val end + @Composable + get() = start + portalText.length + + @Composable + fun urlText() = with(AnnotatedString.Builder()) { + append(infoText) + addStringAnnotation( + tag = "URL", + annotation = portalUri, + start = start, + end = end + ) + addStyle( + SpanStyle(color = AppTheme.colors.primary600), + start, + end + ) + toAnnotatedString() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/QuickFilter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/QuickFilter.kt new file mode 100644 index 00000000..97358aa7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/QuickFilter.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.model + +import kotlinx.serialization.Serializable + +@Serializable +enum class QuickFilter { + OpenNowNearby, DeliveryNearby, Online; +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/SelectedPharmacyUi.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/SelectedPharmacyUi.kt new file mode 100644 index 00000000..c75d45de --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/SelectedPharmacyUi.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.model + +import com.google.maps.android.compose.MarkerState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData + +data class SelectedPharmacyUi( + val item: PharmacyUseCaseData.Pharmacy, + val state: MarkerState? +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyContactPreviewData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyContactPreviewData.kt new file mode 100644 index 00000000..6eba21cc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyContactPreviewData.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.preview + +import androidx.compose.ui.text.buildAnnotatedString +import de.gematik.ti.erp.app.fhir.model.OpeningHours +import de.gematik.ti.erp.app.fhir.model.OpeningTime +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import kotlinx.datetime.LocalTime +import java.time.DayOfWeek + +val mockPharmacy = Pharmacy( + id = "pharmacy", + name = "Test - Pharmacy", + address = null, + coordinates = null, + distance = null, + contacts = PharmacyContacts( + phone = "0123456", + mail = "mail@mail.com", + url = "https://website.com", + pickUpUrl = "https://pickup.com", + deliveryUrl = "https://delivery.com", + onlineServiceUrl = "https://online.com" + ), + provides = listOf(), + openingHours = null, + telematikId = "telematik-id" +) + +val openingTimeA = OpeningTime( + LocalTime.parse("08:00:00"), + LocalTime.parse("12:00:00") +) +val openingTimeB = OpeningTime( + LocalTime.parse("14:00:00"), + LocalTime.parse("18:00:00") +) + +val mockOpeningHours = OpeningHours( + openingTime = mapOf( + DayOfWeek.MONDAY to listOf(openingTimeA, openingTimeB), + DayOfWeek.TUESDAY to listOf(openingTimeA, openingTimeB), + DayOfWeek.WEDNESDAY to listOf(openingTimeA, openingTimeB), + DayOfWeek.THURSDAY to listOf(openingTimeA, openingTimeB), + DayOfWeek.FRIDAY to listOf(openingTimeA, openingTimeB), + DayOfWeek.SATURDAY to listOf(openingTimeA) + ) +) + +val mockDetailedInfoText = buildAnnotatedString { + append("Detailed information about the pharmacy's services and policies.") +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyPreviewParameterProvider.kt new file mode 100644 index 00000000..3721fa85 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacyPreviewParameterProvider.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.fhir.model.OpeningHours +import de.gematik.ti.erp.app.fhir.model.OpeningTime +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.fhir.model.PharmacyService +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.datetime.LocalTime +import java.time.DayOfWeek + +class PharmacyPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + PharmacyPreviewData.ALL_PRESENT_DATA, + PharmacyPreviewData.PICK_UP_ONLY_DATA, + PharmacyPreviewData.DELIVERY_PICKUP_ONLY_DATA, + PharmacyPreviewData.LOCAL_PICKUP_ONLY_DATA + ) +} + +object PharmacyPreviewData { + + private val openingHoursSample = OpeningHours( + openingTime = mapOf( + DayOfWeek.SATURDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ) + ) + ) + + val ALL_PRESENT_DATA = PharmacyUseCaseData.Pharmacy( + id = "1", + name = "Apotheke am Markt", + address = "Marktstraße 1, 12345 Musterstadt", + coordinates = Coordinates(0.0, 0.0), + distance = 1.0, + provides = listOf( + PharmacyService.OnlinePharmacyService(name = "Online"), + PharmacyService.PickUpPharmacyService(name = "PickUp"), + PharmacyService.LocalPharmacyService( + name = "Local", + openingHours = openingHoursSample + ), + PharmacyService.DeliveryPharmacyService( + name = "Delivery", + openingHours = openingHoursSample + ) + ), + openingHours = OpeningHours( + openingTime = mapOf( + DayOfWeek.SATURDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(20, 0, 0) + ) + ), + DayOfWeek.SUNDAY to listOf( + OpeningTime( + openingTime = LocalTime(11, 0, 0), + closingTime = LocalTime(15, 0, 0) + ) + ), + DayOfWeek.MONDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ), + DayOfWeek.TUESDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ), + DayOfWeek.WEDNESDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ), + DayOfWeek.THURSDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ), + DayOfWeek.FRIDAY to listOf( + OpeningTime( + openingTime = LocalTime(8, 0, 0), + closingTime = LocalTime(23, 0, 0) + ) + ) + ) + ), + telematikId = "123456789", + contacts = PharmacyContacts( + phone = "0123456789", + mail = "mpq@nrw.de", + url = "www.apotheke-am-markt.de", + deliveryUrl = "www.apotheke-am-markt.de/lieferung", + onlineServiceUrl = "www.apotheke-am-markt.de/online", + pickUpUrl = "www.apotheke-am-markt.de/abholung" + ) + ) + + val PICK_UP_ONLY_DATA = ALL_PRESENT_DATA.copy( + provides = listOf( + PharmacyService.PickUpPharmacyService(name = "PickUp") + ), + contacts = PharmacyContacts( + phone = "0123456789", + mail = "", + url = "", + deliveryUrl = "", + onlineServiceUrl = "", + pickUpUrl = "" + ) + ) + + val DELIVERY_PICKUP_ONLY_DATA = ALL_PRESENT_DATA.copy( + provides = listOf( + PharmacyService.DeliveryPharmacyService( + name = "Delivery", + openingHours = openingHoursSample + ) + ), + contacts = PharmacyContacts( + phone = "0123456789", + mail = "", + url = "", + deliveryUrl = "www.apotheke-am-markt.de/lieferung", + onlineServiceUrl = "", + pickUpUrl = "www.apotheke-am-markt.de/abholung" + ) + ) + + val LOCAL_PICKUP_ONLY_DATA = ALL_PRESENT_DATA.copy( + provides = listOf( + PharmacyService.LocalPharmacyService( + name = "Local", + openingHours = openingHoursSample + ) + ), + contacts = PharmacyContacts( + phone = "0123456789", + mail = "", + url = "", + deliveryUrl = "www.apotheke-am-markt.de/lieferung", + onlineServiceUrl = "", + pickUpUrl = "www.apotheke-am-markt.de/abholung" + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacySearchListScreenPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacySearchListScreenPreviewParameterProvider.kt new file mode 100644 index 00000000..99eb5dd4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/preview/PharmacySearchListScreenPreviewParameterProvider.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.paging.PagingData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +data class PharmacySearchListScreenPreviewData( + val filter: PharmacyUseCaseData.Filter, + val searchTerm: String, + val isLoading: Boolean, + val pagingData: Flow> +) + +class PharmacySearchListScreenPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + PharmacySearchListScreenPreviewData( + filter = PharmacyUseCaseData.Filter( + openNow = true, + deliveryService = true, + nearBy = true + ), + searchTerm = "Apotheke", + isLoading = false, + pagingData = flowOf(PagingData.empty()) + ), + PharmacySearchListScreenPreviewData( + filter = PharmacyUseCaseData.Filter(), + searchTerm = "Loading", + isLoading = true, + pagingData = flowOf(PagingData.empty()) + ), + PharmacySearchListScreenPreviewData( + filter = PharmacyUseCaseData.Filter( + onlineService = true + ), + searchTerm = "", + isLoading = true, + pagingData = flowOf(PagingData.empty()) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromMessageScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromMessageScreen.kt new file mode 100644 index 00000000..bc3831d5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromMessageScreen.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyDetailsComponent +import de.gematik.ti.erp.app.pharmacy.ui.components.ScreenType +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults + +class PharmacyDetailsFromMessageScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = false) { + @Composable + override fun Content() { + ContentWithCloseButton(onClose = { navController.popBackStack() }) { + PharmacyDetailsComponent( + navController = navController, + navBackStackEntry = navBackStackEntry, + screenType = ScreenType.ForMessage + ) + } + } +} + +@Composable +private fun ContentWithCloseButton( + onClose: () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + Column { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small), + horizontalArrangement = Arrangement.End + ) { + IconButton( + onClick = onClose, + modifier = Modifier + .padding(top = SizeDefaults.one) + ) { + Box( + Modifier + .size(SizeDefaults.fourfold) + .background(AppTheme.colors.neutral100, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Rounded.Close, contentDescription = null, tint = AppTheme.colors.neutral600) + } + } + } + content() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromPharmacyScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromPharmacyScreen.kt new file mode 100644 index 00000000..7c7e19e3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyDetailsFromPharmacyScreen.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyDetailsComponent +import de.gematik.ti.erp.app.pharmacy.ui.components.ScreenType + +class PharmacyDetailsFromPharmacyScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val graphController: PharmacyGraphController +) : BottomSheetScreen() { + @Composable + override fun Content() { + PharmacyDetailsComponent(navController, navBackStackEntry, graphController, ScreenType.ForPharmacy) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyFilterSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyFilterSheetScreen.kt new file mode 100644 index 00000000..a6c5f32a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyFilterSheetScreen.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import android.provider.Settings +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.google.accompanist.flowlayout.FlowRow +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.permissions.getLocationPermissionLauncher +import de.gematik.ti.erp.app.permissions.locationPermissions +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.EMPTY_TASK_ID +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.PHARMACY_NAV_NEARBY_FILTER +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes.PHARMACY_NAV_WITH_START_BUTTON +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType.NEARBY +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationPermissionDeniedDialog +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationServicesNotAvailableDialog +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.Chip +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar + +class PharmacyFilterSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val graphController: PharmacyGraphController +) : BottomSheetScreen(forceToMaxHeight = false) { + @Composable + override fun Content() { + val context = LocalContext.current + + val dialog = LocalDialog.current + + val snackbar = LocalSnackbar.current + + val filter by graphController.filter() + + val locationNotFoundEvent = graphController.locationNotFoundEvent + + val askLocationPermissionEvent = graphController.askLocationPermissionEvent + + val isNearbyFilter = remember { navBackStackEntry.getNearbyFilter() } + + val navWithStartButton = remember { navBackStackEntry.getNavWithSearchButton() } + + val locationPermissionLauncher = getLocationPermissionLauncher( + onPermissionResult = { + graphController.onLocationPermissionResult(it) + } + ) + + LocationPermissionDeniedDialog( + event = graphController.permissionDeniedEvent, + dialog = dialog, + onClick = graphController::forceLocationFalse + ) + + LocationServicesNotAvailableDialog( + event = graphController.serviceDisabledEvent, + dialog = dialog, + onClickDismiss = graphController::forceLocationFalse, + onClickSettings = { + context.openSettingsAsNewActivity( + action = Settings.ACTION_LOCATION_SOURCE_SETTINGS, + isSimpleIntent = true + ) + } + ) + + locationNotFoundEvent.listen { + snackbar.show("Location not found") + } + + askLocationPermissionEvent.listen { + locationPermissionLauncher.launch(locationPermissions) + } + + PharmacyFilterSheetScreenContent( + filter = filter, + isNearbyFilter = isNearbyFilter, + navWithStartButton = navWithStartButton, + onClickFilter = { isFilterChecked, filterType -> + when (filterType) { + NEARBY -> { + when { + isFilterChecked -> graphController.checkLocationServiceAndPermission(context) + else -> graphController.updateFilter(type = filterType, clearLocation = true) + } + } + + else -> graphController.updateFilter(type = filterType) + } + }, + onClickStartSearch = { + navController.navigate(PharmacyRoutes.PharmacySearchListScreen.path(taskId = EMPTY_TASK_ID)) + }, + onBack = { + navController.popBackStack() + } + ) + } +} + +@Composable +private fun PharmacyFilterSheetScreenContent( + filter: PharmacyUseCaseData.Filter, + isNearbyFilter: Boolean, + navWithStartButton: Boolean, + onClickFilter: (Boolean, FilterType) -> Unit, + onClickStartSearch: () -> Unit, + onBack: () -> Unit +) { + Column( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium) + .padding(bottom = PaddingDefaults.Medium) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + stringResource(R.string.search_pharmacies_filter_header), + style = AppTheme.typography.h5 + ) + } + SpacerMedium() + Column(modifier = Modifier.verticalScroll(rememberScrollState(), true)) { + FlowRow( + mainAxisSpacing = PaddingDefaults.Small, + crossAxisSpacing = PaddingDefaults.Small + ) { + if (isNearbyFilter) { + Chip( + stringResource(R.string.search_pharmacies_filter_nearby), + closable = false, + checked = filter.nearBy + ) { + onClickFilter(it, NEARBY) + } + } + Chip( + stringResource(R.string.search_pharmacies_filter_open_now), + closable = false, + checked = filter.openNow + ) { + onClickFilter(it, FilterType.OPEN_NOW) + } + Chip( + stringResource(R.string.search_pharmacies_filter_delivery_service), + closable = false, + checked = filter.deliveryService + ) { + onClickFilter(it, FilterType.DELIVERY_SERVICE) + if (it) { + onClickFilter(it, NEARBY) + } + } + Chip( + stringResource(R.string.search_pharmacies_filter_online_service), + closable = false, + checked = filter.onlineService + ) { + onClickFilter(it, FilterType.ONLINE_SERVICE) + } + } + SpacerMedium() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + PrimaryButtonSmall( + onClick = { + onBack() + if (navWithStartButton) { + onClickStartSearch() + } + } + ) { + Text(stringResource(R.string.search_pharmacies_start_search)) + } + } + SpacerLarge() + } + } +} + +private fun NavBackStackEntry.getNearbyFilter(): Boolean = + arguments?.getBoolean(PHARMACY_NAV_NEARBY_FILTER) ?: false + +private fun NavBackStackEntry.getNavWithSearchButton(): Boolean = + arguments?.getBoolean(PHARMACY_NAV_WITH_START_BUTTON) ?: false + +@LightDarkPreview +@Composable +fun PharmacyFilterSheetScreenPreview() { + PreviewAppTheme { + PharmacyFilterSheetScreenContent( + filter = PharmacyUseCaseData.Filter( + nearBy = true, + openNow = false, + deliveryService = false, + onlineService = true + ), + isNearbyFilter = true, + navWithStartButton = true, + onClickFilter = { _, _ -> + }, + onClickStartSearch = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreen.kt new file mode 100644 index 00000000..19baa887 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreen.kt @@ -0,0 +1,594 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.presentation.WILDCARD +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchListController +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyScreen +import de.gematik.ti.erp.app.pharmacy.ui.components.FilterButtonSection +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyFullScreenSearchLoading +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyMapButton +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyResultCard +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacySearchErrorHint +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacySearchLoading +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacyPreviewData +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacySearchListScreenPreviewData +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacySearchListScreenPreviewParameterProvider +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.pharmacyId +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BorderDivider +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import kotlinx.coroutines.delay + +@Requirement( + "A_20285#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Search results are displayed solely based on search term and user-set filters." +) +class PharmacySearchListScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: PharmacyGraphController +) : PharmacyScreen() { + @Composable + override fun Content() { + val filter by graphController.filter() + val focusManager = LocalFocusManager.current + val location by graphController.coordinates() + + var searchTerm by remember { mutableStateOf(TextFieldValue(WILDCARD)) } + + var isAtLeastOnePharmacyLoaded by remember { mutableStateOf(false) } + + val searchListController = rememberPharmacySearchListController( + filter, + location, + searchTerm.text + ) + + val searchParam by searchListController.searchParamState + + val searchPagingItems = searchListController.pharmaciesState + + val lazyListState = rememberLazyListState() + + val isLoading by remember { + derivedStateOf { + searchPagingItems.loadState.append is LoadState.Loading || + searchPagingItems.loadState.prepend is LoadState.Loading + } + } + + val onBack: () -> Unit = { navController.popBackStack() } + + PharmacySearchListScreenContent( + searchPagingItems = searchPagingItems, + searchTerm = searchTerm, + filter = searchParam.filter, + isLoading = isLoading, + focusManager = focusManager, + lazyListState = lazyListState, + isAtLeastOnePharmacyLoaded = isAtLeastOnePharmacyLoaded, + onSearchInputChange = { + searchTerm = it.copy(selection = TextRange(it.text.length)) + searchListController.onSearchTerm(it.text) + }, + onClickChip = { _, selectedFilter -> + searchListController.onFilter(selectedFilter) { + graphController.updateFilter(it, clearLocation = (selectedFilter == FilterType.NEARBY)) + } + }, + onClickPharmacy = { pharmacy -> + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.path( + pharmacy = pharmacy, + taskId = navBackStackEntry.arguments?.getString(PharmacyRoutes.PHARMACY_NAV_TASK_ID) ?: "" + ) + ) + }, + onClickFilter = { + navController.navigate( + PharmacyRoutes.PharmacyFilterSheetScreen.path( + showNearbyFilter = true, + navigateWithSearchButton = false + ) + ) + }, + onPharmacyLoaded = { isAtLeastOnePharmacyLoaded = true }, + onBack = onBack, + onClickMaps = { + navController.navigate( + PharmacyRoutes.PharmacySearchMapsScreen.path( + taskId = navBackStackEntry.arguments?.getString(PharmacyRoutes.PHARMACY_NAV_TASK_ID) ?: "" + ) + ) + } + ) + } +} + +@Composable +private fun PharmacySearchListScreenContent( + searchTerm: TextFieldValue, + onSearchInputChange: (TextFieldValue) -> Unit, + filter: PharmacyUseCaseData.Filter, + isAtLeastOnePharmacyLoaded: Boolean, + isLoading: Boolean, + focusManager: FocusManager, + lazyListState: LazyListState, + onPharmacyLoaded: () -> Unit, + onClickChip: (Boolean, FilterType) -> Unit, + onClickPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, + searchPagingItems: LazyPagingItems, + onClickFilter: () -> Unit, + onBack: () -> Unit, + onClickMaps: () -> Unit +) { + AnimatedElevationScaffold( + listState = lazyListState, + topBar = { + Column { + SearchTopAppBar( + searchValue = searchTerm, + focusManager = focusManager, + onSearchInputChange = onSearchInputChange, + isLoading = isLoading, + onBack = onBack + ) + FilterButtonSection( + modifier = Modifier.padding(horizontal = PaddingDefaults.Small), + filter = filter, + onClickChip = onClickChip, + onClickFilter = onClickFilter + ) + } + }, + floatingActionButton = { + PharmacyMapButton(onClick = onClickMaps) + } + ) { contentPadding -> + SearchResults( + contentPadding = contentPadding, + searchPagingItems = searchPagingItems, + isAtLeastOnePharmacyLoaded = isAtLeastOnePharmacyLoaded, + lazyListState = lazyListState, + onPharmacyLoaded = onPharmacyLoaded, + onSelectPharmacy = onClickPharmacy + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchTopAppBar( + searchValue: TextFieldValue, + onSearchInputChange: (TextFieldValue) -> Unit, + isLoading: Boolean, + focusManager: FocusManager, + onBack: () -> Unit +) { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = AppTheme.colors.neutral000 + ), + title = { + Column { + PharmacySearchInput( + modifier = Modifier.background(AppTheme.colors.neutral000), + onBack = onBack, + isLoading = isLoading, + focusManager = focusManager, + searchValue = searchValue, + onSearchInputChange = onSearchInputChange + ) + } + } + ) +} + +@Composable +fun PharmacySearchInput( + modifier: Modifier, + isLoading: Boolean, + searchValue: TextFieldValue, + focusManager: FocusManager, + onSearchInputChange: (TextFieldValue) -> Unit, + onBack: () -> Unit +) { + var isLoadingStable by remember { mutableStateOf(isLoading) } + val description = stringResource(id = R.string.pharmacy_search_searchbar) + LaunchedEffect(isLoading) { + delay(timeMillis = 330) + isLoadingStable = isLoading + } + + TextField( + value = searchValue, + onValueChange = { + onSearchInputChange(it) + }, + singleLine = true, + modifier = modifier + .fillMaxWidth() + .semantics { + contentDescription = description + } + .padding(horizontal = PaddingDefaults.Medium), + keyboardOptions = KeyboardOptions( + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions { + onSearchInputChange(searchValue) + focusManager.clearFocus() + }, + shape = RoundedCornerShape(SizeDefaults.double), + textStyle = AppTheme.typography.body1, + leadingIcon = { + val contentDescription = stringResource(R.string.back) + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = contentDescription + ) + } + }, + trailingIcon = { + Crossfade( + isLoadingStable, + animationSpec = tween(durationMillis = 550), + label = "Search Loading Crossfade" + ) { + if (it) { + Box(Modifier.size(SizeDefaults.sixfold)) { + CircularProgressIndicator( + modifier = Modifier + .size(SizeDefaults.triple) + .align(Alignment.Center), + strokeWidth = SizeDefaults.quarter + ) + } + } else { + val contentDescription = stringResource(id = R.string.pharmacy_search_delete_button) + IconButton( + onClick = { onSearchInputChange(TextFieldValue(WILDCARD)) } + ) { + Icon( + Icons.Rounded.Close, + contentDescription = contentDescription + ) + } + } + } + }, + colors = TextFieldDefaults.textFieldColors( + textColor = AppTheme.colors.neutral900, + leadingIconColor = AppTheme.colors.neutral600, + trailingIconColor = AppTheme.colors.neutral600, + backgroundColor = AppTheme.colors.neutral100, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) +} + +@Composable +private fun SearchResults( + modifier: Modifier = Modifier, + contentPadding: PaddingValues, + isAtLeastOnePharmacyLoaded: Boolean, + lazyListState: LazyListState, + onPharmacyLoaded: () -> Unit, + searchPagingItems: LazyPagingItems, + onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit +) { + val errorTitle = stringResource(R.string.search_pharmacy_error_title) + val errorSubtitle = stringResource(R.string.search_pharmacy_error_subtitle) + val errorAction = stringResource(R.string.search_pharmacy_error_action) + + val loadState = searchPagingItems.loadState + + val noPharmacies by remember(loadState, searchPagingItems.itemCount) { + derivedStateOf { + listOf(loadState.prepend, loadState.append) + .all { + when (it) { + is LoadState.NotLoading -> + it.endOfPaginationReached && searchPagingItems.itemCount == 0 + + else -> false + } + } && loadState.refresh is LoadState.NotLoading + } + } + + val showError by remember { + derivedStateOf { searchPagingItems.itemCount <= 1 && loadState.refresh is LoadState.Error } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag(TestTag.PharmacySearch.ResultContent), + state = lazyListState, + contentPadding = contentPadding + ) { + if (noPharmacies) { + NoPharmaciesFoundCard() + } + if (showError) { + PharmacySearchErrorCard( + isFullScreen = true, + title = errorTitle, + subtitle = errorSubtitle, + action = errorAction, + onClickAction = { searchPagingItems.retry() } + ) + } + if (loadState.prepend is LoadState.Error || loadState.append is LoadState.Error) { + PharmacySearchErrorCard( + isFullScreen = false, + title = errorTitle, + subtitle = errorSubtitle, + action = errorAction, + onClickAction = { searchPagingItems.retry() } + ) + } + items( + count = searchPagingItems.itemCount, + key = searchPagingItems.itemKey { it.id } + ) { index -> + Crossfade( + targetState = searchPagingItems[index], + animationSpec = tween(durationMillis = 550), + label = "Search Loading Crossfade" + ) { pharmacy -> + pharmacy?.let { + onPharmacyLoaded() + PharmacyListSearchResult( + modifier = modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + count = searchPagingItems.itemCount, + index = index, + pharmacy = pharmacy, + onSelectPharmacy = onSelectPharmacy + ) + } + } + } + if (loadState.refresh is LoadState.Loading) { + PharmacyFullScreenSearchLoading() + } + if (loadState.append is LoadState.Loading) { + if (isAtLeastOnePharmacyLoaded) { + PharmacySearchLoading() + } else { + PharmacyFullScreenSearchLoading() + } + } + } +} + +@Composable +private fun PharmacyListSearchResult( + modifier: Modifier, + count: Int, + index: Int, + pharmacy: PharmacyUseCaseData.Pharmacy, + onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit +) { + Column { + PharmacyResultCard( + modifier = modifier + .semantics { + pharmacyId = pharmacy.telematikId + } + .testTag(TestTag.PharmacySearch.PharmacyListEntry), + pharmacy = pharmacy + ) { + onSelectPharmacy(pharmacy) + } + BorderDivider() + if (index < count - 1) { + BorderDivider() + } + } +} + +@Suppress("FunctionName") +private fun LazyListScope.NoPharmaciesFoundCard() { + item { + PharmacySearchErrorHint( + title = stringResource(R.string.search_pharmacy_nothing_found_header), + subtitle = stringResource(R.string.search_pharmacy_nothing_found_info), + modifier = Modifier + .fillMaxWidth() + .fillParentMaxHeight() + .imePadding() + ) + } +} + +@Suppress("FunctionName") +private fun LazyListScope.PharmacySearchErrorCard( + title: String, + subtitle: String, + action: String?, + onClickAction: () -> Unit, + isFullScreen: Boolean = false +) { + item { + PharmacySearchErrorHint( + title = title, + subtitle = subtitle, + action = action, + onClickAction = onClickAction, + modifier = Modifier + .fillMaxWidth() + .let { + if (isFullScreen) { + it.fillParentMaxHeight() + } else { + it.padding(PaddingDefaults.Medium) + } + } + ) + } +} + +@LightDarkPreview +@Composable +fun PharmacySearchListScreenContentPreview( + @PreviewParameter( + PharmacySearchListScreenPreviewParameterProvider::class + ) previewData: PharmacySearchListScreenPreviewData +) { + PreviewAppTheme { + val pagingItems = previewData.pagingData.collectAsLazyPagingItems() + PharmacySearchListScreenContent( + lazyListState = rememberLazyListState(), + searchTerm = TextFieldValue(previewData.searchTerm), + filter = previewData.filter, + isLoading = previewData.isLoading, + focusManager = LocalFocusManager.current, + searchPagingItems = pagingItems, + isAtLeastOnePharmacyLoaded = false, + onSearchInputChange = {}, + onPharmacyLoaded = {}, + onClickChip = { _, _ -> }, + onClickPharmacy = {}, + onClickFilter = {}, + onBack = {}, + onClickMaps = {} + ) + } +} + +@LightDarkPreview +@Composable +fun PharmacyListSearchResultPreview() { + PreviewAppTheme { + Column( + modifier = Modifier.padding(PaddingDefaults.Small) + ) { + PharmacyListSearchResult( + modifier = Modifier, + count = 3, + index = 0, + pharmacy = PharmacyPreviewData.ALL_PRESENT_DATA, + onSelectPharmacy = {} + ) + SpacerMedium() + PharmacyListSearchResult( + modifier = Modifier, + count = 3, + index = 1, + pharmacy = PharmacyPreviewData.LOCAL_PICKUP_ONLY_DATA, + onSelectPharmacy = {} + ) + SpacerMedium() + PharmacyListSearchResult( + modifier = Modifier, + count = 3, + index = 2, + pharmacy = PharmacyPreviewData.LOCAL_PICKUP_ONLY_DATA, + onSelectPharmacy = {} + ) + SpacerMedium() + PharmacyListSearchResult( + modifier = Modifier, + count = 3, + index = 3, + pharmacy = PharmacyPreviewData.LOCAL_PICKUP_ONLY_DATA, + onSelectPharmacy = {} + ) + SpacerMedium() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchMapsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchMapsScreen.kt new file mode 100644 index 00000000..f85b00ea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchMapsScreen.kt @@ -0,0 +1,452 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import android.graphics.Point +import android.provider.Settings +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.maps.android.SphericalUtil +import com.google.maps.android.compose.CameraPositionState +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerInfoWindowContent +import com.google.maps.android.compose.MarkerState +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.permissions.getLocationPermissionLauncher +import de.gematik.ti.erp.app.permissions.isLocationPermissionAndServiceEnabled +import de.gematik.ti.erp.app.permissions.locationPermissions +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchMapsController +import de.gematik.ti.erp.app.pharmacy.presentation.toCoordinates +import de.gematik.ti.erp.app.pharmacy.presentation.toLatLng +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyScreen +import de.gematik.ti.erp.app.pharmacy.ui.components.DefaultZoomLevel +import de.gematik.ti.erp.app.pharmacy.ui.components.GooglePharmacyMap +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationPermissionDeniedDialog +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationServicesNotAvailableDialog +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyMap +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyProperties +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacySettings +import de.gematik.ti.erp.app.pharmacy.ui.components.PositionState +import de.gematik.ti.erp.app.pharmacy.ui.components.pharmacyMapsOverlay +import de.gematik.ti.erp.app.pharmacy.ui.model.SelectedPharmacyUi +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.disableZoomWhileActive +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar +import de.gematik.ti.erp.app.utils.letNotNull +import io.github.aakira.napier.Napier +import org.kodein.di.compose.rememberInstance + +private const val MyLocationZoomLevel = 15f +private const val AnimationDuration = 730 + +class PharmacySearchMapsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: PharmacyGraphController +) : PharmacyScreen() { + + @Composable + override fun Content() { + val snackbar = LocalSnackbar.current + val dialog = LocalDialog.current + val context = LocalContext.current + LocalActivity.current.disableZoomWhileActive() + + val mapHolder by rememberInstance() + val filter by graphController.filter() + val coordinates by graphController.coordinates() + val previewCoordinates by graphController.previewCoordinates() + + val mapsListController = rememberPharmacySearchMapsController(filter, coordinates) + + val locationNotFoundEvent = graphController.locationNotFoundEvent + val areMapsLoadingEvent = mapsListController.areMapsLoadingEvent + val pharmacies by mapsListController.pharmaciesState + val searchState by mapsListController.searchParamState + + var isLoading by remember { mutableStateOf(false) } + var isMapLocationEnabled by remember { mutableStateOf(false) } + var showSearchButton by remember { mutableStateOf(false) } + + // NOTE: forcing it as a Google map, could lead to crashes if the map is not a Google map + val cameraPositionState by (mapHolder as GooglePharmacyMap).cameraPositionState + val cameraRadius by mapsListController.cameraRadiusState + var mapZoomState by remember { mutableFloatStateOf(0f) } + var selectedPharmacy by remember { mutableStateOf(null) } + var radiusFromSearch by remember { mutableDoubleStateOf(0.0) } + val positionState = remember(coordinates, previewCoordinates) { + PositionState( + position = coordinates ?: previewCoordinates, + zoom = DefaultZoomLevel + ) + } + + val properties = remember(isMapLocationEnabled) { + PharmacyProperties.Default + .copy(isMyLocationEnabled = isMapLocationEnabled) + } + + val locationPermissionLauncher = getLocationPermissionLauncher( + onPermissionResult = { isLocationEnabled -> + isMapLocationEnabled = isLocationEnabled + graphController.onLocationPermissionResult(isLocationEnabled) + } + ) + + LaunchedEffect(true) { + locationPermissionLauncher.launch(locationPermissions) + } + + areMapsLoadingEvent.listen { + isLoading = it + } + + LocationPermissionDeniedDialog( + event = graphController.permissionDeniedEvent, + dialog = dialog, + onClick = graphController::forceLocationFalse + ) + + LocationServicesNotAvailableDialog( + event = graphController.serviceDisabledEvent, + dialog = dialog, + onClickDismiss = graphController::forceLocationFalse, + onClickSettings = { + context.openSettingsAsNewActivity( + action = Settings.ACTION_LOCATION_SOURCE_SETTINGS, + isSimpleIntent = true + ) + } + ) + + locationNotFoundEvent.listen { + snackbar.show(context.getString(R.string.location_not_found)) + } + + cameraPositionState?.let { + CameraAnimation( + coordinates = coordinates ?: previewCoordinates, + cameraPositionState = it, + searchState = searchState, + pharmacies = pharmacies + ) { + showSearchButton = true + } + } + + val onBack: () -> Unit = { + navController.popBackStack() + } + + BackHandler { onBack() } + + PharmacySearchMapsScreenContent( + cameraRadius = cameraRadius, + coordinates = coordinates, + isLoading = isLoading, + mapHolder = mapHolder, + mapZoomState = mapZoomState, + positionState = positionState, + pharmacyProperties = properties, + pharmacies = pharmacies, + selectedPharmacy = selectedPharmacy, + showSearchButton = showSearchButton, + isMapLocationEnabled = isMapLocationEnabled, + onBack = onBack, + onZoomStateChanged = { + mapZoomState = it + }, + onClickFilter = { + navController.navigate( + PharmacyRoutes.PharmacyFilterSheetScreen.path( + showNearbyFilter = false, + navigateWithSearchButton = false + ) + ) + }, + onClickPharmacy = { selectedPharmacyUi -> + selectedPharmacy = selectedPharmacyUi + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.path( + pharmacy = selectedPharmacyUi.item, + taskId = navBackStackEntry.arguments?.getString(PharmacyRoutes.PHARMACY_NAV_TASK_ID) ?: "" + ) + ) + }, + onSearch = { isLocationSearch -> + if (isLoading.isFalse()) { + radiusFromSearch = SphericalUtil.computeDistanceBetween( + cameraPositionState?.projection?.visibleRegion?.latLngBounds?.northeast, + cameraPositionState?.projection?.visibleRegion?.latLngBounds?.southwest + ) / 2.0 + + if (isLocationSearch) { + mapsListController.onCameraRadiusChanged(radiusFromSearch) + locationPermissionLauncher.launch(locationPermissions) + } else { + mapsListController.onCameraRadiusChanged(radiusFromSearch) + cameraPositionState?.position?.target?.let { camLatLng -> + graphController.forceLocationFalseWithSelectedCoordinates(camLatLng.toCoordinates()) + } + } + } + } + ) + } + + companion object { + private fun Boolean.isFalse() = !this + } +} + +@Suppress("LongParameterList") +@Composable +private fun PharmacySearchMapsScreenContent( + mapHolder: PharmacyMap, + positionState: PositionState, + pharmacyProperties: PharmacyProperties, + pharmacies: List, + selectedPharmacy: SelectedPharmacyUi?, + showSearchButton: Boolean, + isLoading: Boolean, + isMapLocationEnabled: Boolean, + cameraRadius: Double, + mapZoomState: Float, + coordinates: Coordinates?, + onClickPharmacy: (SelectedPharmacyUi) -> Unit, + onSearch: (Boolean) -> Unit, + onClickFilter: () -> Unit, + onZoomStateChanged: (Float) -> Unit, + onBack: () -> Unit +) { + Scaffold(contentWindowInsets = WindowInsets.Companion.systemBars) { paddings -> + Box { + mapHolder.Map( + modifier = Modifier + .padding( + start = paddings.calculateStartPadding(LocalLayoutDirection.current), + end = paddings.calculateEndPadding(LocalLayoutDirection.current) + ) + .fillMaxSize(), + isFullScreen = true, + onZoomStateChanged = onZoomStateChanged, + positionState = positionState, + settings = PharmacySettings.FullScreen, + properties = pharmacyProperties, + contentPaddingValues = WindowInsets.Companion.systemBars.asPaddingValues(), + onClick = null, + content = { + key(pharmacies.hashCode()) { + MarkedPharmacies( + cameraRadius = cameraRadius, + mapZoomState = mapZoomState, + coordinates = coordinates, + pharmacyMapsResult = pharmacies, + onClickMarker = onClickPharmacy + ) + } + key(selectedPharmacy) { + selectedPharmacy?.let { selectedPharmacy -> + SelectedPharmacy( + cameraRadius = cameraRadius, + mapZoomState = mapZoomState, + coordinates = coordinates, + pharmacy = selectedPharmacy, + onClickMarker = onClickPharmacy + ) + } + } + if (isMapLocationEnabled) { + coordinates?.let { coordinates -> + MarkerInfoWindowContent { + Marker( + state = MarkerState(coordinates.toLatLng()), + icon = null + ) + } + } + } + } + ) + pharmacyMapsOverlay( + showSearchButton = showSearchButton, + isLoading = isLoading, + onSearch = onSearch, + onClickFilter = onClickFilter, + onBack = onBack + ) + } + } +} + +@Composable +private fun SelectedPharmacy( + cameraRadius: Double, + mapZoomState: Float, + coordinates: Coordinates?, + pharmacy: SelectedPharmacyUi, + onClickMarker: (SelectedPharmacyUi) -> Unit +) { + val markerIcon = remember { BitmapDescriptorFactory.fromResource(R.drawable.maps_marker_red) } + pharmacy.item.coordinates?.let { pharmacyCoordinates -> + // the remember needs the keys given to adjust the markers to user interactions + val state = remember(cameraRadius, pharmacy, mapZoomState, coordinates) { + if (pharmacy.state != null) { + MarkerState(pharmacy.state.position) + } else { + val latLng = LatLng(pharmacyCoordinates.latitude, pharmacyCoordinates.longitude) + MarkerState(latLng) + } + } + Marker( + state = state, + onClick = { + onClickMarker(pharmacy.copy(state = state)) + false + }, + icon = markerIcon, + zIndex = 9999f + ) + } +} + +@Composable +private fun MarkedPharmacies( + cameraRadius: Double, + mapZoomState: Float, + coordinates: Coordinates?, + pharmacyMapsResult: List, + onClickMarker: (SelectedPharmacyUi) -> Unit +) { + val markerIcon = remember { BitmapDescriptorFactory.fromResource(R.drawable.maps_marker) } + pharmacyMapsResult.mapNotNull { pharmacy -> + pharmacy.coordinates?.let { pharmacyCoordinates -> + // the remember needs the keys given to adjust the markers to user interactions + val marker = remember(cameraRadius, pharmacyMapsResult, mapZoomState, coordinates) { + val latLng = LatLng(pharmacyCoordinates.latitude, pharmacyCoordinates.longitude) + MarkerState(latLng) + } + Marker( + state = marker, + icon = markerIcon, + onClick = { + onClickMarker(SelectedPharmacyUi(pharmacy, marker)) + false + } + ) + } + } +} + +@Composable +private fun CameraAnimation( + coordinates: Coordinates, + cameraPositionState: CameraPositionState, + searchState: PharmacyUseCaseData.MapsSearchData, + pharmacies: List, + onShowSearchButton: () -> Unit +) { + val context = LocalContext.current + var lastMarkerCenter by remember { mutableStateOf(coordinates.toLatLng()) } + val isMoving by remember(cameraPositionState.isMoving) { + derivedStateOf { cameraPositionState.isMoving } + } + + val moveDistance = with(LocalDensity.current) { SizeDefaults.triple.roundToPx() } + LaunchedEffect(isMoving) { + cameraPositionState.projection?.let { projection -> + val latLng = + (searchState.locationMode as? PharmacyUseCaseData.LocationMode.Enabled)?.toLatLng() + ?: lastMarkerCenter + val distanceBetween = SphericalUtil.computeDistanceBetween(cameraPositionState.position.target, latLng) + val locationLatLng = projection.fromScreenLocation(Point(0, 0)) + val movedDistanceLatLng = projection.fromScreenLocation(Point(moveDistance, 0)) + if (distanceBetween >= SphericalUtil.computeDistanceBetween(locationLatLng, movedDistanceLatLng)) { + onShowSearchButton() + } + } + } + + LaunchedEffect(pharmacies) { + try { + if (context.isLocationPermissionAndServiceEnabled()) { + val latitudeAndLongitude = + (searchState.locationMode as? PharmacyUseCaseData.LocationMode.Enabled) + ?.toLatLng() + ?: lastMarkerCenter + cameraPositionState.animate( + CameraUpdateFactory.newLatLngZoom(latitudeAndLongitude, MyLocationZoomLevel), + durationMs = AnimationDuration + ) + } else if (pharmacies.isNotEmpty()) { + val bounds = LatLngBounds.builder().apply { + pharmacies.forEach { + letNotNull(it.coordinates?.latitude, it.coordinates?.longitude) { latitude, longitude -> + include(LatLng(latitude, longitude)) + } + } + }.build() + val latLng = coordinates.toLatLng() + cameraPositionState.animate( + update = CameraUpdateFactory.newLatLngZoom(latLng, MyLocationZoomLevel), + durationMs = AnimationDuration + ) + CameraUpdateFactory.zoomBy(MyLocationZoomLevel) + lastMarkerCenter = bounds.center + } + } catch (e: SecurityException) { + Napier.e { "Security exception: Missing permissions ${e.stackTraceToString()}" } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyStartScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyStartScreen.kt new file mode 100644 index 00000000..e6cdd098 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacyStartScreen.kt @@ -0,0 +1,496 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import android.provider.Settings +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.loading.LoadingIndicator +import de.gematik.ti.erp.app.permissions.getLocationPermissionLauncher +import de.gematik.ti.erp.app.permissions.isLocationPermissionAndServiceEnabled +import de.gematik.ti.erp.app.permissions.locationPermissions +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.model.SelectedFavouritePharmacyState +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRouteBackStackEntryArguments +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.presentation.FilterType +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyGraphController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyStartController +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyScreen +import de.gematik.ti.erp.app.pharmacy.ui.components.FavouritePharmacies +import de.gematik.ti.erp.app.pharmacy.ui.components.FilterSection +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationPermissionDeniedDialog +import de.gematik.ti.erp.app.pharmacy.ui.components.LocationServicesNotAvailableDialog +import de.gematik.ti.erp.app.pharmacy.ui.components.MapsTile +import de.gematik.ti.erp.app.pharmacy.ui.components.MapsTitle +import de.gematik.ti.erp.app.pharmacy.ui.components.MockMap +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacyMap +import de.gematik.ti.erp.app.pharmacy.ui.components.PharmacySearchButton +import de.gematik.ti.erp.app.pharmacy.ui.model.QuickFilter +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.capitalizeFirstChar +import de.gematik.ti.erp.app.utils.extensions.isGooglePlayServiceAvailable +import de.gematik.ti.erp.app.utils.letNotNull +import kotlinx.datetime.Instant +import org.kodein.di.compose.rememberInstance + +class PharmacyStartScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + override val graphController: PharmacyGraphController +) : PharmacyScreen() { + + @Suppress("CyclomaticComplexMethod") + @Composable + override fun Content() { + val isModalFlow = navBackStackEntry.arguments?.getBoolean( + PharmacyRoutes.PHARMACY_NAV_SHOW_BACK_ON_START_SCREEN + ) ?: false + + val context = LocalContext.current + + val controller = rememberPharmacyStartController() + + val selectedPharmacyState by controller.selectedPharmacyState.collectAsStateWithLifecycle() + + val retryEvent = ComposableEvent() + val acceptMissingEvent = ComposableEvent() + var showLoadingIndicator by remember { mutableStateOf(false) } + + val favouritePharmacies by graphController.favouritePharmacies() + val previewCoordinates by graphController.previewCoordinates() + val isGooglePlayServicesAvailable = context.isGooglePlayServiceAvailable() + + var quickFilterSelectedWithoutLocation by remember { + mutableStateOf(null) + } + + // Allows us to switch between the real implementation and a mock implementation of PharmacyMap + val pharmacyMap by rememberInstance() + + LaunchedEffect(Unit) { + graphController.init(context) + } + + val locationPermissionLauncher = getLocationPermissionLauncher( + onPermissionResult = { + graphController.onLocationPermissionResult(it) + } + ) + + LocationPermissionDeniedDialog( + event = graphController.permissionDeniedEvent, + dialog = dialog, + onClick = { + graphController.forceLocationFalse() + quickFilterSelectedWithoutLocation = null + } + ) + + LocationServicesNotAvailableDialog( + event = graphController.serviceDisabledEvent, + dialog = dialog, + onClickDismiss = { + graphController.forceLocationFalse() + quickFilterSelectedWithoutLocation = null + }, + onClickSettings = { + quickFilterSelectedWithoutLocation = null + context.openSettingsAsNewActivity( + action = Settings.ACTION_LOCATION_SOURCE_SETTINGS, + isSimpleIntent = true + ) + } + ) + + graphController.askLocationPermissionEvent.listen { + locationPermissionLauncher.launch(locationPermissions) + } + + graphController.locationProvidedEvent.listen { + when (quickFilterSelectedWithoutLocation) { + // NEARBY filter is already set when location was enabled + QuickFilter.OpenNowNearby -> updateOpenNowNearbyFilterAndNavigationToSearchList() + QuickFilter.DeliveryNearby -> updateDeliveryNearbyFilterAndNavigationToSearchList() + else -> { + // do nothing + } + } + } + + PharmacyNotReachableDialog( + event = retryEvent, + dialogScaffold = dialog, + onRetry = controller::getPharmacy + ) + + PharmacyNotFoundDialog( + event = acceptMissingEvent, + dialogScaffold = dialog, + onClickAccept = controller::clearSelectedPharmacy + ) + + when (selectedPharmacyState) { + is SelectedFavouritePharmacyState.Loading -> { + showLoadingIndicator = true + } + + is SelectedFavouritePharmacyState.Error -> { + controller.resetSelectedPharmacyState() + retryEvent.trigger() + } + + is SelectedFavouritePharmacyState.Data -> { + controller.resetSelectedPharmacyState() + letNotNull( + (selectedPharmacyState as SelectedFavouritePharmacyState.Data).pharmacy, + PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) { pharmacy, taskId -> + navController.navigate( + PharmacyRoutes.PharmacyDetailsFromPharmacyScreen.path( + pharmacy = pharmacy, + taskId = taskId + ) + ) + } + } + + SelectedFavouritePharmacyState.Idle -> { + showLoadingIndicator = false + } + + SelectedFavouritePharmacyState.Missing -> { + controller.resetSelectedPharmacyState() + acceptMissingEvent.trigger() + } + } + + Box { + PharmacyStartScreenContent( + isModalFlow = isModalFlow, + favouritePharmacies = favouritePharmacies, + listState = listState, + isGooglePlayServicesAvailable = isGooglePlayServicesAvailable, + previewCoordinates = previewCoordinates, + previewMap = pharmacyMap, + onBack = { + if (isModalFlow) { + graphController.reset() + navController.popBackStack() + } + }, + onClickPharmacySearch = { + navController.navigate( + PharmacyRoutes.PharmacySearchListScreen.path( + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + }, + onClickMapsSearch = { + navController.navigate( + PharmacyRoutes.PharmacySearchMapsScreen.path( + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + }, + onClickQuickFilterSearch = { quickFilter -> + when (quickFilter) { + QuickFilter.OpenNowNearby -> { + if (context.isLocationPermissionAndServiceEnabled()) { + // NEARBY filter needs to be set since it is not set when location was enabled long ago + graphController.updateFilter(FilterType.NEARBY) + updateOpenNowNearbyFilterAndNavigationToSearchList() + } else { + quickFilterSelectedWithoutLocation = QuickFilter.OpenNowNearby + graphController.checkLocationServiceAndPermission(context) + } + } + + QuickFilter.DeliveryNearby -> { + if (context.isLocationPermissionAndServiceEnabled()) { + // NEARBY filter needs to be set since it is not set when location was enabled long ago + graphController.updateFilter(FilterType.NEARBY) + updateDeliveryNearbyFilterAndNavigationToSearchList() + } else { + quickFilterSelectedWithoutLocation = QuickFilter.DeliveryNearby + graphController.checkLocationServiceAndPermission(context) + } + } + + QuickFilter.Online -> { + updateOnlineFilterAndNavigateToSearchList() + } + } + }, + onClickFilter = { + navController.navigate( + PharmacyRoutes.PharmacyFilterSheetScreen.path( + showNearbyFilter = true, + navigateWithSearchButton = true + ) + ) + }, + onClickFavouritePharmacy = { + controller.onPharmacySelected(it) + } + ) + + if (showLoadingIndicator) { + LoadingIndicator() + } + } + } + + private fun updateOpenNowNearbyFilterAndNavigationToSearchList() { + graphController.updateFilter(FilterType.OPEN_NOW) + navController.navigate( + PharmacyRoutes.PharmacySearchListScreen.path( + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + } + + private fun updateDeliveryNearbyFilterAndNavigationToSearchList() { + graphController.updateFilter(FilterType.DELIVERY_SERVICE) + navController.navigate( + PharmacyRoutes.PharmacySearchListScreen.path( + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + } + + private fun updateOnlineFilterAndNavigateToSearchList() { + graphController.updateFilter(FilterType.ONLINE_SERVICE) + navController.navigate( + PharmacyRoutes.PharmacySearchListScreen.path( + taskId = PharmacyRouteBackStackEntryArguments(navBackStackEntry).getTaskId() + ) + ) + } +} + +@Composable +private fun PharmacyStartScreenContent( + isModalFlow: Boolean, + favouritePharmacies: List = emptyList(), + previewCoordinates: Coordinates, + previewMap: PharmacyMap, + listState: LazyListState, + isGooglePlayServicesAvailable: Boolean = true, + onClickQuickFilterSearch: (QuickFilter) -> Unit, + onClickFavouritePharmacy: (OverviewPharmacy) -> Unit, + onClickPharmacySearch: () -> Unit, + onClickMapsSearch: () -> Unit, + onClickFilter: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + isModalFlow = isModalFlow, + topBarTitle = stringResource(R.string.redeem_header).capitalizeFirstChar(), + listState = listState, + onBack = onBack, + content = { contentPadding -> + PharmacyStartScreenBody( + contentPadding = contentPadding, + favouritePharmacies = favouritePharmacies, + previewCoordinates = previewCoordinates, + previewMap = previewMap, + listState = listState, + isGooglePlayServicesAvailable = isGooglePlayServicesAvailable, + onClickPharmacySearch = onClickPharmacySearch, + onClickMapsSearch = onClickMapsSearch, + onClickQuickFilterSearch = onClickQuickFilterSearch, + onClickFilter = onClickFilter, + onClickFavouritePharmacy = onClickFavouritePharmacy + ) + } + ) +} + +@Composable +private fun PharmacyStartScreenBody( + contentPadding: PaddingValues, + previewCoordinates: Coordinates, + previewMap: PharmacyMap, + favouritePharmacies: List, + listState: LazyListState, + isGooglePlayServicesAvailable: Boolean, + onClickPharmacySearch: () -> Unit, + onClickMapsSearch: () -> Unit, + onClickQuickFilterSearch: (QuickFilter) -> Unit, + onClickFilter: () -> Unit, + onClickFavouritePharmacy: (OverviewPharmacy) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + state = listState, + contentPadding = contentPadding + ) { + item { + SpacerLarge() + } + PharmacySearchButton( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium), + onStartSearch = onClickPharmacySearch + ) + if (isGooglePlayServicesAvailable) { + MapsTitle() + MapsTile( + previewCoordinates = previewCoordinates, + previewMap = previewMap + ) { onClickMapsSearch() } + } + FilterSection( + onClickFilter = onClickFilter, + onSelectFilter = onClickQuickFilterSearch + ) + if (favouritePharmacies.isNotEmpty()) { + FavouritePharmacies( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium) + .fillMaxWidth(), + pharmacies = favouritePharmacies + ) { + onClickFavouritePharmacy(it) + } + } + item { + SpacerLarge() + } + } +} + +@Composable +private fun PharmacyNotReachableDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onRetry: () -> Unit +) { + event.listen { + dialogScaffold.show { + ErezeptAlertDialog( + title = stringResource(R.string.pharmacy_search_apovz_call_no_internet_header), + bodyText = stringResource(R.string.pharmacy_search_apovz_call_no_internet_info), + dismissText = stringResource(R.string.pharmacy_search_apovz_call_no_internet_cancel), + confirmText = stringResource(R.string.pharmacy_search_apovz_call_no_internet_retry), + onDismissRequest = it::dismiss, + onConfirmRequest = { + it.dismiss() + onRetry() + } + ) + } + } +} + +@Composable +private fun PharmacyNotFoundDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onClickAccept: () -> Unit +) { + event.listen { + dialogScaffold.show { + ErezeptAlertDialog( + title = stringResource(R.string.pharmacy_search_apovz_call_failed_header), + body = stringResource(R.string.pharmacy_search_apovz_call_failed_body), + onDismissRequest = { + onClickAccept() + it.dismiss() + } + ) + } + } +} + +@Suppress("MagicNumber") +@LightDarkPreview +@Composable +fun PharmacyStartScreenPreview() { + val time = Instant.parse("2022-01-01T00:00:00Z") + val berlin = Coordinates(52.51947562977698, 13.404335795642881) + + val pharmacyMap = MockMap() + + PreviewAppTheme { + Column { + PharmacyStartScreenContent( + favouritePharmacies = listOf( + OverviewPharmacy( + lastUsed = time, + isFavorite = true, + usageCount = 1, + telematikId = "123456789", + pharmacyName = "Berlin Apotheke", + address = "BerlinStr, 12345 Berlin" + ), + OverviewPharmacy( + lastUsed = time, + isFavorite = false, + usageCount = 1, + telematikId = "123456788", + pharmacyName = "Stuttgart Apotheke", + address = "StuttgartStr, 12345 Stuttgart" + ) + ), + previewCoordinates = berlin, + previewMap = pharmacyMap, + listState = rememberLazyListState(), + isGooglePlayServicesAvailable = true, + onClickQuickFilterSearch = {}, + onClickFavouritePharmacy = {}, + onClickPharmacySearch = {}, + onClickMapsSearch = {}, + onClickFilter = {}, + onBack = {}, + isModalFlow = false + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/ChangePharmacyFavoriteStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/ChangePharmacyFavoriteStateUseCase.kt new file mode 100644 index 00000000..dc9e3bad --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/ChangePharmacyFavoriteStateUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ChangePharmacyFavoriteStateUseCase( + private val repository: PharmacyRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(pharmacy: PharmacyUseCaseData.Pharmacy, isFavorite: Boolean) { + withContext(dispatcher) { + when { + isFavorite -> repository.markPharmacyAsFavourite(pharmacy) + else -> repository.deleteFavoritePharmacy(pharmacy) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/DeleteOverviewPharmacyUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/DeleteOverviewPharmacyUseCase.kt new file mode 100644 index 00000000..57b32788 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/DeleteOverviewPharmacyUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class DeleteOverviewPharmacyUseCase( + private val repository: PharmacyRepository, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(pharmacy: OverviewPharmacyData.OverviewPharmacy) { + withContext(dispatchers) { + repository.deleteOverviewPharmacy(pharmacy) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetLocationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetLocationUseCase.kt new file mode 100644 index 00000000..a2c3b87a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetLocationUseCase.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import android.content.Context +import android.location.Location +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.google.android.gms.tasks.CancellationTokenSource +import de.gematik.ti.erp.app.permissions.isLocationPermissionGranted +import de.gematik.ti.erp.app.permissions.isLocationServiceEnabled +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine + +class GetLocationUseCase( + private val context: Context +) { + @OptIn(ExperimentalCoroutinesApi::class) + suspend operator fun invoke(): LocationResult = + suspendCancellableCoroutine { continuation -> + try { + val isServiceEnabled = context.isLocationServiceEnabled() + if (!isServiceEnabled) { + continuation.resume(LocationResult.ServiceDisabled, null) + } else { + val isPermissionGranted = context.isLocationPermissionGranted() + if (!isPermissionGranted) { + continuation.resume(LocationResult.PermissionDenied, null) + } else { + val cancelTokenSource = CancellationTokenSource() + + // client needs a permission check to work without crashing + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) + + fusedLocationClient + .getCurrentLocation(Priority.PRIORITY_BALANCED_POWER_ACCURACY, cancelTokenSource.token) + .addOnSuccessListener { location -> + if (location != null) { + continuation.resume(LocationResult.Success(location), null) + } else { + continuation.resume(LocationResult.LocationNotFound, null) + } + } + .addOnFailureListener { + Napier.e { "Location error on suspension ${it.stackTraceToString()}" } + continuation.resume(LocationResult.LocationNotFound, null) + } + } + } + } catch (e: SecurityException) { + Napier.e { "Location error ${e.stackTraceToString()}" } + if (!continuation.isCancelled) { + continuation.cancel(e) + } + } + } + + sealed interface LocationResult { + data class Success(val location: Location) : LocationResult + data object ServiceDisabled : LocationResult + data object PermissionDenied : LocationResult + data object LocationNotFound : LocationResult + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt index 654ee5c7..c3c2009d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase @@ -58,8 +58,8 @@ class GetOrderStateUseCase( profileRepository.activeProfile().flatMapLatest { profile -> combine( shippingContactRepository.shippingContact(), - getRedeemedSyncedTasks(profile.id), - getRedeemedScannedTasks(profile.id) + getRedeemableSyncedTasks(profile.id), + getRedeemableScannedTasks(profile.id) ) { contact, syncedTasks, scannedTasks -> val updatedContact = when { syncedTasks.isNotEmpty() && contact == null -> @@ -68,9 +68,11 @@ class GetOrderStateUseCase( else -> contact } val orders = syncedTasks.map { it.toOrder() } + scannedTasks.map { it.toOrder() } + val selfPayerPrescriptionIds = orders.filter { it.isSelfPayerPrescription }.map { it.taskId } val shippingContact = updatedContact?.toModel() ?: EmptyShippingContact PharmacyUseCaseData.OrderState( - orders = orders, + prescriptionOrders = orders, + selfPayerPrescriptionIds = selfPayerPrescriptionIds, contact = shippingContact ) }.flowOn(dispatcher) @@ -82,18 +84,21 @@ class GetOrderStateUseCase( return this } - private fun getRedeemedSyncedTasks(id: ProfileIdentifier): Flow> = + private fun getRedeemableSyncedTasks(id: ProfileIdentifier): Flow> = prescriptionRepository.syncedTasks(id) .mapNotNull { tasks -> tasks.filter { it.redeemState().isRedeemable() } + .sortedByDescending { it.authoredOn } }.flowOn(dispatcher) - private fun getRedeemedScannedTasks(id: ProfileIdentifier): Flow> = + private fun getRedeemableScannedTasks(id: ProfileIdentifier): Flow> = prescriptionRepository.scannedTasks(id) .mapNotNull { tasks -> tasks.filter { it.isRedeemable() - it.communications.isEmpty() + // TODO: (Check) Keeping this comment in-case we need the check for redeem enabled + // it.communications.isEmpty() } + .sortedByDescending { it.scannedOn } }.flowOn(dispatcher) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt index 609e3632..bb970067 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase @@ -32,17 +32,28 @@ class GetOverviewPharmaciesUseCase( private val repository: PharmacyRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - operator fun invoke(): Flow> { - val result = combine( - repository.loadOftenUsedPharmacies(), - repository.loadFavoritePharmacies() - ) { oftenUsedPharmacies, favouritePharmacies -> - (oftenUsedPharmacies + favouritePharmacies).filter { mixedPharmacy -> - val booleanResult = favouritePharmacies.any { it.telematikId == mixedPharmacy.telematikId } - booleanResult - }.distinctBy { it.telematikId } - .take(LAST_USED_PHARMACIES_COUNT) - }.flowOn(dispatcher) - return result + operator fun invoke(): Flow> = combine( + repository.loadFavoritePharmacies(), + repository.loadOftenUsedPharmacies() + ) { favouritePharmacies, oftenUsedPharmacies -> + val favourites = favouritePharmacies + .sortByLastUsed() + + val oftenUsedOnes = oftenUsedPharmacies + .notInFavourites(favouritePharmacies) + .sortByLastUsed() + + (favourites + oftenUsedOnes) + .distinctBy { it.telematikId } + .take(LAST_USED_PHARMACIES_COUNT) + }.flowOn(dispatcher) + + companion object { + + fun List.sortByLastUsed() = sortedByDescending { it.lastUsed } + + fun List.notInFavourites( + favourites: List + ) = filter { it.telematikId !in favourites.map { fav -> fav.telematikId } } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPharmacyByTelematikIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPharmacyByTelematikIdUseCase.kt new file mode 100644 index 00000000..dfad47d9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPharmacyByTelematikIdUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GetPharmacyByTelematikIdUseCase( + private val repository: PharmacyRepository, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(telematikId: String) = + withContext(dispatchers) { + repository.searchPharmacyByTelematikId(telematikId) + .map { it.pharmacies.toModel().firstOrNull() } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPreviewMapCoordinatesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPreviewMapCoordinatesUseCase.kt new file mode 100644 index 00000000..36a95147 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetPreviewMapCoordinatesUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.repository.PreviewMapCoordinatesRepository + +class GetPreviewMapCoordinatesUseCase( + private val repository: PreviewMapCoordinatesRepository +) { + operator fun invoke() = repository.getPreviewCoordinates() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/IsPharmacyFavoriteUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/IsPharmacyFavoriteUseCase.kt new file mode 100644 index 00000000..d4d47a6b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/IsPharmacyFavoriteUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class IsPharmacyFavoriteUseCase( + private val repository: PharmacyRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = + repository.isPharmacyInFavorites(pharmacy) + .flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt index 04ef55fd..b7e8d408 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase @@ -28,7 +28,6 @@ import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.fhir.model.CommunicationPayload import de.gematik.ti.erp.app.fhir.model.createCommunicationDispenseRequest import de.gematik.ti.erp.app.pharmacy.model.PharmacyData -import de.gematik.ti.erp.app.pharmacy.model.shippingContact import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository import de.gematik.ti.erp.app.pharmacy.usecase.mapper.PharmacyInitialResultsPerPage @@ -40,18 +39,12 @@ import de.gematik.ti.erp.app.prescription.repository.RemoteRedeemOption import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.repository.SettingsRepository -import kotlinx.coroutines.Dispatchers +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import java.util.UUID import kotlin.math.max -// can't be modified; the backend will always return 80 entries on the first page -const val PharmacyInitialResultsPerPage = 80 -const val PharmacyNextResultsPerPage = 10 - class PharmacySearchUseCase( private val repository: PharmacyRepository, private val shippingContactRepository: ShippingContactRepository, @@ -61,6 +54,11 @@ class PharmacySearchUseCase( ) { data class PharmacyPagingKey(val bundleId: String, val offset: Int) + @Requirement( + "A_20285#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "pharmacy search paging based on search term and filter criteria set by the user." + ) inner class PharmacyPagingSource(searchData: PharmacyUseCaseData.SearchData) : PagingSource() { @@ -69,7 +67,8 @@ class PharmacySearchUseCase( private val filter = run { val filterMap = mutableMapOf() if (locationMode is PharmacyUseCaseData.LocationMode.Enabled) { - filterMap += "near" to "${locationMode.location.latitude}|${locationMode.location.longitude}|999|km" + filterMap += "near" to "" + + "${locationMode.coordinates.latitude}|${locationMode.coordinates.longitude}|999|km" } if (searchData.filter.directRedeem) { filterMap += "type" to "DELEGATOR" @@ -107,6 +106,7 @@ class PharmacySearchUseCase( ) }.getOrElse { LoadResult.Error(it) } } + is LoadParams.Append, is LoadParams.Prepend -> { val key = params.key!! @@ -135,11 +135,9 @@ class PharmacySearchUseCase( } @Requirement( - "A_20182", - "A_20183", - "A_20208", + "A_20285#4", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Search results are only based on search term and filter criteria set by the user." + rationale = "pharmacy search based on search term and filter criteria set by the user." ) suspend fun searchPharmacies( searchData: PharmacyUseCaseData.SearchData @@ -164,69 +162,6 @@ class PharmacySearchUseCase( ).flow.flowOn(dispatchers.io) } - fun prescriptionDetailsForOrdering( - profileId: ProfileIdentifier - ): Flow = - combine( - shippingContactRepository.shippingContact(), - prescriptionRepository.syncedTasks(profileId).map { tasks -> - tasks.filter { - it.redeemState().isRedeemable() - } - }, - prescriptionRepository.scannedTasks(profileId).map { tasks -> - tasks.filter { - it.isRedeemable() - it.communications.isEmpty() - } - } - ) { shippingContacts, syncedTasks, scannedTasks -> - - val shippingContact = if (syncedTasks.isNotEmpty()) { - shippingContacts ?: run { - syncedTasks.first().shippingContact().also { - shippingContactRepository.saveShippingContact(it) - } - } - } else { - shippingContacts - } - - val tasks = scannedTasks.map { task -> - PharmacyUseCaseData.PrescriptionOrder( - taskId = task.taskId, - accessCode = task.accessCode, - title = task.name, - index = task.index, - timestamp = task.scannedOn, - substitutionsAllowed = false - ) - } + syncedTasks.map { task -> - PharmacyUseCaseData.PrescriptionOrder( - taskId = task.taskId, - accessCode = task.accessCode!!, // TODO: check, why we get here a nullable!! - title = task.medicationName(), - index = null, - timestamp = task.authoredOn, - substitutionsAllowed = false - ) - } - - PharmacyUseCaseData.OrderState( - tasks, - PharmacyUseCaseData.ShippingContact( - name = shippingContact?.name ?: "", - line1 = shippingContact?.line1 ?: "", - line2 = shippingContact?.line2 ?: "", - postalCode = shippingContact?.postalCode ?: "", - city = shippingContact?.city ?: "", - telephoneNumber = shippingContact?.telephoneNumber ?: "", - mail = shippingContact?.mail ?: "", - deliveryInformation = shippingContact?.deliveryInformation ?: "" - ) - ) - }.flowOn(Dispatchers.Default) - suspend fun saveShippingContact(contact: PharmacyUseCaseData.ShippingContact) { shippingContactRepository.saveShippingContact( mapShippingContact(contact) @@ -241,7 +176,7 @@ class PharmacySearchUseCase( contact: PharmacyUseCaseData.ShippingContact, pharmacyTelematikId: String ): Result { - val comDisp = createCommunicationDispenseRequest( + val communicationDispenseRequestJson = createCommunicationDispenseRequest( orderId = orderId.toString(), taskId = order.taskId, accessCode = order.accessCode, @@ -256,7 +191,21 @@ class PharmacySearchUseCase( ) ) - return prescriptionRepository.redeemPrescription(profileId, comDisp, accessCode = order.accessCode) + val result = runCatching { + prescriptionRepository.redeemPrescription( + profileId = profileId, + communication = communicationDispenseRequestJson, + accessCode = order.accessCode + ) + }.map { + if (it.isSuccess) { + val result = it.getOrNull() + Napier.d { "Redeem prescription successful: $result" } + } else { + throw it.exceptionOrNull() ?: IllegalStateException("Redeem prescription failed") + } + } + return result } private fun mapShippingContact(contact: PharmacyUseCaseData.ShippingContact) = diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SaveShippingContactUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SaveShippingContactUseCase.kt new file mode 100644 index 00000000..63dcb5b8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SaveShippingContactUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SaveShippingContactUseCase( + private val repository: ShippingContactRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(contact: PharmacyUseCaseData.ShippingContact) { + withContext(dispatcher) { + repository.saveShippingContact(contact.model()) + } + } + + companion object { + private fun PharmacyUseCaseData.ShippingContact.model() = + PharmacyData.ShippingContact( + name = name.trim(), + line1 = line1.trim(), + line2 = line2.trim(), + postalCode = postalCode.trim(), + city = city.trim(), + telephoneNumber = telephoneNumber.trim(), + mail = mail.trim(), + deliveryInformation = deliveryInformation.trim() + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SetPreviewMapCoordinatesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SetPreviewMapCoordinatesUseCase.kt new file mode 100644 index 00000000..3af624fa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/SetPreviewMapCoordinatesUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.repository.PreviewMapCoordinatesRepository + +class SetPreviewMapCoordinatesUseCase( + private val repository: PreviewMapCoordinatesRepository +) { + operator fun invoke(coordinates: Coordinates?) { + repository.setPreviewCoordinates(coordinates) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt index a096eb1c..76e88b8f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt index e46e458e..1629ed67 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv @@ -26,34 +26,45 @@ import de.gematik.ti.erp.app.consent.usecase.GetConsentUseCase import de.gematik.ti.erp.app.consent.usecase.GrantConsentUseCase import de.gematik.ti.erp.app.consent.usecase.RevokeConsentUseCase import de.gematik.ti.erp.app.consent.usecase.SaveGrantConsentDrawerShownUseCase -import de.gematik.ti.erp.app.consent.usecase.ShowGrantConsentUseCase +import de.gematik.ti.erp.app.consent.usecase.ShowGrantConsentDrawerUseCase +import de.gematik.ti.erp.app.invoice.repository.DefaultInvoiceRepository import de.gematik.ti.erp.app.invoice.repository.InvoiceLocalDataSource import de.gematik.ti.erp.app.invoice.repository.InvoiceRemoteDataSource import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.invoice.usecase.DeleteAllLocalInvoices +import de.gematik.ti.erp.app.invoice.usecase.DeleteInvoiceUseCase +import de.gematik.ti.erp.app.invoice.usecase.DownloadInvoicesUseCase import de.gematik.ti.erp.app.invoice.usecase.GetInvoiceByTaskIdUseCase -import de.gematik.ti.erp.app.invoice.usecase.InvoiceUseCase +import de.gematik.ti.erp.app.invoice.usecase.GetInvoicesByProfileUseCase +import de.gematik.ti.erp.app.invoice.usecase.SaveInvoiceUseCase +import de.gematik.ti.erp.app.pkv.presentation.ConsentController +import de.gematik.ti.erp.app.pkv.usecase.ShareInvoiceUseCase import org.kodein.di.DI import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton import org.kodein.di.instance val pkvModule = DI.Module("pkvModule") { bindProvider { GetConsentUseCase(instance()) } bindProvider { GrantConsentUseCase(instance()) } bindProvider { RevokeConsentUseCase(instance()) } - bindProvider { ShowGrantConsentUseCase(instance(), instance()) } + bindProvider { ShowGrantConsentDrawerUseCase(instance(), instance()) } bindProvider { SaveGrantConsentDrawerShownUseCase(instance()) } - - bindProvider { DefaultConsentRepository(instance(), instance()) } - bindProvider { ConsentLocalDataSource(instance()) } - bindProvider { ConsentRemoteDataSource(instance()) } - - bindProvider { InvoiceUseCase(instance(), instance()) } - bindProvider { InvoiceRepository(instance(), instance(), instance()) } + bindProvider { DownloadInvoicesUseCase(instance(), instance()) } + bindProvider { DeleteInvoiceUseCase(instance(), instance()) } + bindProvider { DeleteAllLocalInvoices(instance()) } + bindProvider { GetInvoiceByTaskIdUseCase(instance()) } + bindProvider { GetInvoicesByProfileUseCase(instance()) } + bindProvider { SaveInvoiceUseCase(instance()) } + bindProvider { ShareInvoiceUseCase(instance()) } + bindProvider { DefaultInvoiceRepository(instance(), instance(), instance()) } bindProvider { InvoiceRemoteDataSource(instance()) } bindProvider { InvoiceLocalDataSource(instance()) } - bindProvider { GetInvoiceByTaskIdUseCase(instance()) } + bindSingleton { ConsentController(instance(), instance(), instance(), instance()) } } val consentRepositoryModule = DI.Module("consentRepositoryModule", allowSilentOverride = true) { bindProvider { DefaultConsentRepository(instance(), instance()) } + bindProvider { ConsentLocalDataSource(instance()) } + bindProvider { ConsentRemoteDataSource(instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceAction.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceAction.kt new file mode 100644 index 00000000..e396dcf3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceAction.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.model + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier + +sealed class InvoiceAction { + data class Submit(val taskId: String, val profileIdentifier: ProfileIdentifier) : InvoiceAction() + data class Share(val taskId: String, val profileIdentifier: ProfileIdentifier) : InvoiceAction() + data class Correct(val taskId: String, val profileIdentifier: ProfileIdentifier) : InvoiceAction() + data class InAppCorrect(val taskId: String, val profileIdentifier: ProfileIdentifier) : InvoiceAction() + data class Delete(val taskId: String) : InvoiceAction() + data class ViewDetail(val taskId: String) : InvoiceAction() + data object ViewList : InvoiceAction() + data object Back : InvoiceAction() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceState.kt new file mode 100644 index 00000000..c7b3dfe9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/model/InvoiceState.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.model + +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +sealed class InvoiceState { + data object LoadingOnChange : InvoiceState() + data object NoInvoice : InvoiceState() + class InvoiceLoaded(val record: InvoiceData.PKVInvoiceRecord) : InvoiceState() + + @Composable + @OptIn(ExperimentalContracts::class) + inline fun OnInvoiceLoaded( + invoices: @Composable (InvoiceLoaded) -> Unit + ) { + contract { + returns(true) implies (this@InvoiceState is InvoiceLoaded) + returns(false) implies (this@InvoiceState is NoInvoice) + } + if (this is InvoiceLoaded) { + invoices(this) + } + } + + @OptIn(ExperimentalContracts::class) + fun hasInvoice(): Boolean { + contract { + returns(true) implies (this@InvoiceState is InvoiceLoaded) + returns(false) implies (this@InvoiceState is NoInvoice) + } + return this is InvoiceLoaded + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvGraph.kt index 47a366a6..908ad427 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv.navigation @@ -22,11 +22,15 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import de.gematik.ti.erp.app.navigation.renderComposable -import de.gematik.ti.erp.app.pkv.ui.InvoiceDetailsScreen -import de.gematik.ti.erp.app.pkv.ui.InvoiceExpandedDetailsScreen -import de.gematik.ti.erp.app.pkv.ui.InvoiceListScreen -import de.gematik.ti.erp.app.pkv.ui.InvoiceLocalCorrectionScreen -import de.gematik.ti.erp.app.pkv.ui.InvoiceShareScreen +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideInRight +import de.gematik.ti.erp.app.navigation.slideOutLeft +import de.gematik.ti.erp.app.navigation.slideOutUp +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceDetailsScreen +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceExpandedDetailsScreen +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceListScreen +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceLocalCorrectionScreen +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceShareScreen fun NavGraphBuilder.pkvGraph( startDestination: String = PkvRoutes.InvoiceListScreen.route, @@ -38,7 +42,9 @@ fun NavGraphBuilder.pkvGraph( ) { renderComposable( route = PkvRoutes.InvoiceListScreen.route, - arguments = PkvRoutes.InvoiceListScreen.arguments + arguments = PkvRoutes.InvoiceListScreen.arguments, + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() } ) { navEntry -> InvoiceListScreen( navController = navController, @@ -47,7 +53,9 @@ fun NavGraphBuilder.pkvGraph( } renderComposable( route = PkvRoutes.InvoiceDetailsScreen.route, - arguments = PkvRoutes.InvoiceDetailsScreen.arguments + arguments = PkvRoutes.InvoiceDetailsScreen.arguments, + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() } ) { navEntry -> InvoiceDetailsScreen( navController = navController, @@ -56,7 +64,9 @@ fun NavGraphBuilder.pkvGraph( } renderComposable( route = PkvRoutes.InvoiceExpandedDetailsScreen.route, - arguments = PkvRoutes.InvoiceExpandedDetailsScreen.arguments + arguments = PkvRoutes.InvoiceExpandedDetailsScreen.arguments, + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() } ) { navEntry -> InvoiceExpandedDetailsScreen( navController = navController, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvRoutes.kt index 04b86fc9..08583fa2 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/navigation/PkvRoutes.kt @@ -1,71 +1,93 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv.navigation +import android.os.Bundle import androidx.navigation.NavType import androidx.navigation.navArgument import de.gematik.ti.erp.app.navigation.NavigationRouteNames import de.gematik.ti.erp.app.navigation.NavigationRoutes import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes.getPkvNavigationProfileId +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes.getPkvNavigationTaskId +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier object PkvRoutes : NavigationRoutes { override fun subGraphName() = "invoices" - const val TaskId = "taskId" - const val ProfileId = "profileId" + const val PKV_NAV_TASK_ID = "taskId" + const val PKV_NAV_PROFILE_ID = "profileId" + object InvoiceListScreen : Routes( NavigationRouteNames.InvoiceListScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PKV_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PKV_NAV_PROFILE_ID to profileId) } + object InvoiceDetailsScreen : Routes( NavigationRouteNames.InvoiceDetailsScreen.name, - navArgument(TaskId) { type = NavType.StringType }, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PKV_NAV_TASK_ID) { type = NavType.StringType }, + navArgument(PKV_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(taskId: String, profileId: String) = path(TaskId to taskId, ProfileId to profileId) + fun path(taskId: String, profileId: String) = path(PKV_NAV_TASK_ID to taskId, PKV_NAV_PROFILE_ID to profileId) } + object InvoiceExpandedDetailsScreen : Routes( NavigationRouteNames.InvoiceExpandedDetailsScreen.name, - navArgument(TaskId) { type = NavType.StringType }, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PKV_NAV_TASK_ID) { type = NavType.StringType }, + navArgument(PKV_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(taskId: String, profileId: String) = path(TaskId to taskId, ProfileId to profileId) + fun path(taskId: String, profileId: String) = path(PKV_NAV_TASK_ID to taskId, PKV_NAV_PROFILE_ID to profileId) } + object InvoiceLocalCorrectionScreen : Routes( NavigationRouteNames.InvoiceLocalCorrectionScreen.name, - navArgument(TaskId) { type = NavType.StringType }, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PKV_NAV_TASK_ID) { type = NavType.StringType }, + navArgument(PKV_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(taskId: String, profileId: String) = path(TaskId to taskId, ProfileId to profileId) + fun path(taskId: String, profileId: String) = path(PKV_NAV_TASK_ID to taskId, PKV_NAV_PROFILE_ID to profileId) } + object InvoiceShareScreen : Routes( NavigationRouteNames.InvoiceShareScreen.name, - navArgument(TaskId) { type = NavType.StringType }, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PKV_NAV_TASK_ID) { type = NavType.StringType }, + navArgument(PKV_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(taskId: String, profileId: String) = path(TaskId to taskId, ProfileId to profileId) + fun path(taskId: String, profileId: String) = path(PKV_NAV_TASK_ID to taskId, PKV_NAV_PROFILE_ID to profileId) } + + fun (Bundle?).getPkvNavigationTaskId(): String? = this?.getString(PKV_NAV_TASK_ID) + fun (Bundle?).getPkvNavigationProfileId(): ProfileIdentifier = requireNotNull(this?.getString(PKV_NAV_PROFILE_ID)) } -data class PkvNavigationArguments(val taskId: String, val profileId: String) +data class PkvNavigationArguments(val taskId: String?, val profileId: ProfileIdentifier) { + companion object { + fun (Bundle?).getPkvNavigationArguments(): PkvNavigationArguments = + this?.let { bundle -> + PkvNavigationArguments( + taskId = bundle.getPkvNavigationTaskId(), + profileId = requireNotNull(bundle.getPkvNavigationProfileId()) { "ProfileId is required to be sent for this screen" } + ) + } ?: throw IllegalArgumentException("PKV Bundle is null") + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentController.kt index d07ba75c..de9198f1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentController.kt @@ -1,197 +1,120 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv.presentation -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.api.HTTP_BAD_REQUEST -import de.gematik.ti.erp.app.api.HTTP_CONFLICT -import de.gematik.ti.erp.app.api.HTTP_FORBIDDEN -import de.gematik.ti.erp.app.api.HTTP_INTERNAL_ERROR -import de.gematik.ti.erp.app.api.HTTP_METHOD_NOT_ALLOWED -import de.gematik.ti.erp.app.api.HTTP_NOT_FOUND -import de.gematik.ti.erp.app.api.HTTP_REQUEST_TIMEOUT -import de.gematik.ti.erp.app.api.HTTP_TOO_MANY_REQUESTS -import de.gematik.ti.erp.app.api.HTTP_UNAUTHORIZED +import androidx.compose.runtime.saveable.rememberSaveable +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isConsentGranted +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isNotGranted import de.gematik.ti.erp.app.consent.usecase.GetConsentUseCase import de.gematik.ti.erp.app.consent.usecase.GrantConsentUseCase import de.gematik.ti.erp.app.consent.usecase.RevokeConsentUseCase -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState +import de.gematik.ti.erp.app.consent.usecase.SaveGrantConsentDrawerShownUseCase +import de.gematik.ti.erp.app.core.complexAutoSaver import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance -import java.net.UnknownHostException @Stable class ConsentController( - val context: Context, - val profileId: ProfileIdentifier, - val insuranceIdentifier: String, private val getConsentUseCase: GetConsentUseCase, private val grantConsentUseCase: GrantConsentUseCase, private val revokeConsentUseCase: RevokeConsentUseCase, - private val scope: CoroutineScope -) { + private val saveGrantConsentDrawerShownUseCase: SaveGrantConsentDrawerShownUseCase +) : Controller() { - private val consentMutableState = MutableStateFlow(ConsentState.UnknownConsent) - - private val consentErrorMutableState = MutableStateFlow(ConsentErrorState.NoError) - - val consentState: StateFlow = consentMutableState - - val consentErrorState: StateFlow = consentErrorMutableState - - sealed interface ConsentState : PrescriptionServiceState { - - // before getting the consent - data object UnknownConsent : ConsentState - data object NotGranted : ConsentState - data object Granted : ConsentState - data object Revoked : ConsentState + private val _consentState by lazy { + MutableStateFlow( + ConsentState.ValidState.UnknownConsent + ) } - sealed interface ConsentErrorState : PrescriptionServiceErrorState { + val consentState: StateFlow = _consentState - data object NoError : ConsentErrorState - data object AlreadyGranted : ConsentErrorState - data object ChargeConsentAlreadyRevoked : ConsentErrorState - data object InternalError : ConsentErrorState - data object ServerTimeout : ConsentErrorState - data object Unauthorized : ConsentErrorState - data object TooManyRequests : ConsentState - data object NoInternet : ConsentErrorState - data object BadRequest : ConsentErrorState - data object Forbidden : ConsentErrorState - data object Unknown : ConsentErrorState - } + val isConsentGranted: StateFlow = consentState.map { it.isConsentGranted() } + .stateIn(controllerScope, started = SharingStarted.Eagerly, initialValue = false) - private val isConsentGranted by lazy { - consentMutableState.value == ConsentState.Granted || - consentErrorState.value == ConsentErrorState.AlreadyGranted + val isConsentNotGranted by lazy { + consentState.map { it.isNotGranted() } + .stateIn( + controllerScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) } - val isConsentGrantedState - @Composable - get() = isConsentGranted - - fun getChargeConsent() { - scope.launch { + fun getChargeConsent(profileId: ProfileIdentifier) { + _consentState.value = ConsentState.ValidState.Loading + controllerScope.launch { getConsentUseCase(profileId) - .fold( - onSuccess = { result -> - when (result) { - true -> ConsentState.Granted - else -> ConsentState.NotGranted - } - setNoError() - }, - onFailure = { - mapConsentErrorStates(it) - } - ) + .first().apply { + _consentState.value = this as ConsentState + } } } - fun grantChargeConsent() { - scope.launch { - grantConsentUseCase(profileId, insuranceIdentifier) - .fold( - onSuccess = { - consentMutableState.value = ConsentState.Granted - setNoError() - }, - onFailure = { - mapConsentErrorStates(it) - } - ) + fun grantChargeConsent(profileId: ProfileIdentifier) { + controllerScope.launch { + grantConsentUseCase(profileId).first().apply { + _consentState.value = this as ConsentState + } } } - private fun setNoError() { - consentErrorMutableState.value = ConsentErrorState.NoError - } - - fun revokeChargeConsent() { - scope.launch { + fun revokeChargeConsent(profileId: ProfileIdentifier) { + controllerScope.launch { revokeConsentUseCase(profileId) - .fold( - onSuccess = { - consentMutableState.value = ConsentState.Revoked - }, - onFailure = { - mapConsentErrorStates(it) - } - ) + .first().apply { + _consentState.value = this as ConsentState + } } } - private fun mapConsentErrorStates(error: Throwable) { - if (error.cause?.cause is UnknownHostException) { - consentErrorMutableState.value = ConsentErrorState.NoInternet - } else { - val errorCode = (error as? ApiCallException)?.response?.code() - consentErrorMutableState.value = when (errorCode) { - HTTP_CONFLICT -> ConsentErrorState.AlreadyGranted - HTTP_REQUEST_TIMEOUT -> ConsentErrorState.ServerTimeout - HTTP_INTERNAL_ERROR -> ConsentErrorState.InternalError - HTTP_TOO_MANY_REQUESTS -> ConsentErrorState.TooManyRequests - HTTP_NOT_FOUND -> ConsentErrorState.ChargeConsentAlreadyRevoked - HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED -> ConsentErrorState.BadRequest - HTTP_FORBIDDEN -> ConsentErrorState.Forbidden - HTTP_UNAUTHORIZED -> ConsentErrorState.Unauthorized - else -> { - ConsentErrorState.Unknown // silent fail - } - } + fun saveConsentDrawerShown(profileId: ProfileIdentifier) { + controllerScope.launch { + saveGrantConsentDrawerShownUseCase.invoke(profileId) } } } @Composable -fun rememberConsentController(profile: ProfilesUseCaseData.Profile): ConsentController { - val context = LocalContext.current - +fun rememberConsentController(): ConsentController { val getConsentUseCase by rememberInstance() val grantConsentUseCase by rememberInstance() val revokeConsentUseCase by rememberInstance() + val saveGrantConsentDrawerShownUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - return remember(profile.id, profile.insurance.insuranceIdentifier) { + return rememberSaveable(saver = complexAutoSaver()) { ConsentController( - context = context, - profileId = profile.id, - insuranceIdentifier = profile.insurance.insuranceIdentifier, getConsentUseCase = getConsentUseCase, grantConsentUseCase = grantConsentUseCase, revokeConsentUseCase = revokeConsentUseCase, - scope = scope + saveGrantConsentDrawerShownUseCase = saveGrantConsentDrawerShownUseCase ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentValidator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentValidator.kt new file mode 100644 index 00000000..23967468 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/ConsentValidator.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.presentation + +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isConsentGranted +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isNotGranted + +// call this on screens that need consent in a LaunchEffect or/and on pull to refresh +object ConsentValidator { + + fun validateAndExecute( + isSsoTokenValid: Boolean, + consentState: ConsentState, + getChargeConsent: () -> Unit, + onConsentGranted: () -> Unit, + grantConsent: (() -> Unit)? = null + ) { + if (!isSsoTokenValid) return + when { + consentState is ConsentState.ValidState.UnknownConsent || consentState is ConsentState.ValidState.NotGranted -> getChargeConsent() + consentState.isConsentGranted() -> onConsentGranted() + consentState.isNotGranted() -> grantConsent?.invoke() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceController.kt index 779f719d..e5460f07 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv.presentation @@ -21,179 +21,307 @@ package de.gematik.ti.erp.app.pkv.presentation import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator -import de.gematik.ti.erp.app.core.LocalAuthenticator +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.api.HttpErrorState.ErrorWithCause +import de.gematik.ti.erp.app.authentication.model.ChooseAuthenticationController +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.collectResult +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isConsentGranted +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate -import de.gematik.ti.erp.app.invoice.usecase.InvoiceUseCase +import de.gematik.ti.erp.app.invoice.model.InvoiceResult +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceError +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceSuccess.SuccessOnDeletion +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.UserNotLoggedInError +import de.gematik.ti.erp.app.invoice.usecase.DeleteAllLocalInvoices +import de.gematik.ti.erp.app.invoice.usecase.DeleteInvoiceUseCase +import de.gematik.ti.erp.app.invoice.usecase.DownloadInvoicesUseCase +import de.gematik.ti.erp.app.invoice.usecase.GetInvoiceByTaskIdUseCase +import de.gematik.ti.erp.app.invoice.usecase.GetInvoicesByProfileUseCase import de.gematik.ti.erp.app.pkv.FileProviderAuthority -import de.gematik.ti.erp.app.pkv.usecase.createSharableFileInCache -import de.gematik.ti.erp.app.pkv.usecase.sharePDFFile -import de.gematik.ti.erp.app.pkv.usecase.writePDFAttachments -import de.gematik.ti.erp.app.pkv.usecase.writePdfFromHtml -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.ui.RefreshedState -import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions -import de.gematik.ti.erp.app.prescription.presentation.retryWithAuthenticator -import de.gematik.ti.erp.app.profiles.presentation.ProfileController +import de.gematik.ti.erp.app.pkv.presentation.model.InvoiceCardUiState +import de.gematik.ti.erp.app.pkv.usecase.ShareInvoiceUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase -import de.gematik.ti.erp.app.utils.asFhirTemporal -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance -import java.net.HttpURLConnection class InvoiceController( - profileId: ProfileIdentifier, - private val invoiceUseCase: InvoiceUseCase, - private val getProfilesUseCase: GetProfilesUseCase, - private val authenticator: Authenticator, - private val fileProviderAuthority: FileProviderAuthority, - private val scope: CoroutineScope - + getProfileByIdUseCase: GetProfileByIdUseCase, + getProfilesUseCase: GetProfilesUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + biometricAuthenticator: BiometricAuthenticator, + private val profileId: ProfileIdentifier, + private val downloadInvoicesUseCase: DownloadInvoicesUseCase, + private val getInvoicesByProfileUseCase: GetInvoicesByProfileUseCase, + private val getInvoiceByTaskIdUseCase: GetInvoiceByTaskIdUseCase, + private val deleteInvoiceUseCase: DeleteInvoiceUseCase, + private val deleteAllInvoicesUseCase: DeleteAllLocalInvoices, + private val shareInvoiceUseCase: ShareInvoiceUseCase, + val invoiceDetailScreenEvents: InvoiceDetailScreenEvents = InvoiceDetailScreenEvents(), + val invoiceListScreenEvents: InvoiceListScreenEvents = InvoiceListScreenEvents() +) : ChooseAuthenticationController( + profileId = profileId, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator ) { - private val profiles by lazy { - getProfilesUseCase().stateIn(scope, SharingStarted.Lazily, listOf(ProfileController.DEFAULT_EMPTY_PROFILE)) + private val getInvoicesTrigger: MutableStateFlow = MutableStateFlow(false) + private val _isRefreshing = MutableStateFlow(false) + + private fun refreshGetInvoices() { + getInvoicesTrigger.value = !getInvoicesTrigger.value } - @Composable - fun getProfilesState() = profiles.collectAsStateWithLifecycle() - sealed interface State : PrescriptionServiceState { + private fun getInvoices() = getInvoicesByProfileUseCase.invoke(profileId).map { UiState.Data(it) } - object InvoiceDeleted : State + private fun completeDownloadEvent() { + refreshGetInvoices() + invoiceListScreenEvents.downloadCompletedEvent.trigger() + } - sealed interface Error : State, PrescriptionServiceErrorState { - object InvoiceAlreadyDeleted : Error - } - fun PrescriptionServiceState.isInvoiceDeleted() = - this == InvoiceDeleted || this == Error.InvoiceAlreadyDeleted + @OptIn(ExperimentalCoroutinesApi::class) + val invoices: StateFlow>>> by lazy { + getInvoicesTrigger.flatMapLatest { getInvoices() } + .stateIn( + controllerScope, + started = SharingStarted.Eagerly, + initialValue = UiState.Loading() + ) } - private val stateFlow: Flow>> = - invoiceUseCase.invoices(profileId) + val isRefreshing: StateFlow = _isRefreshing - val state - @Composable - get() = stateFlow.collectAsStateWithLifecycle(null) + init { + refreshGetInvoices() - fun detailState(taskId: String): Flow = - invoiceUseCase.invoiceById(taskId) + biometricAuthenticationSuccessEvent.listen(controllerScope) { + downloadInvoices() + invoiceListScreenEvents.getConsentEvent.trigger(profileId) + } - val isRefreshing - @Composable - get() = invoiceUseCase.refreshInProgress.collectAsStateWithLifecycle() + biometricAuthenticationResetErrorEvent.listen(controllerScope) { error -> + refreshGetInvoices() + invoiceListScreenEvents.showAuthenticationErrorDialog.trigger(error) + } - fun downloadInvoices( - profileId: ProfileIdentifier - ): Flow = - invoiceUseCase.downloadInvoices(profileId) - .map { - RefreshedState(it) - } - .retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) + biometricAuthenticationOtherErrorEvent.listen(controllerScope) { error -> + invoiceListScreenEvents.showAuthenticationErrorDialog.trigger(error) + } - fun shareInvoicePDF( - context: Context, - invoice: InvoiceData.PKVInvoice, - fileProviderAuthority: FileProviderAuthority + onRefreshProfileAction.listen(controllerScope) { + _isRefreshing.value = it + } + } + + fun getInvoiceForTaskId(taskId: String): Flow = + getInvoiceByTaskIdUseCase(taskId) + + fun deleteInvoice( + taskId: String, + profileId: ProfileIdentifier = this.profileId ) { - val html = PkvHtmlTemplate.createHTML(invoice) - val file = createSharableFileInCache(context, "invoices", "invoice") - scope.launch { - writePdfFromHtml(context, "Invoice_${invoice.taskId}", html, file) - invoiceUseCase.loadAttachments(invoice.taskId)?.let { - writePDFAttachments(file, it) - } - val subject = invoice.medicationRequest.medication?.name() + "_" + - invoice.timestamp.asFhirTemporal().formattedString() - sharePDFFile(context, file, subject, fileProviderAuthority) + controllerScope.launch { + deleteInvoiceUseCase.invoke(taskId, profileId) + .fold( + onSuccess = { result -> + when (result) { + is SuccessOnDeletion -> invoiceDetailScreenEvents.deleteSuccessfulEvent.trigger() + is InvoiceError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(result) + else -> Napier.e { "wrong state reached on invoice delete $result" } // state should not be reached + } + }, + onFailure = { error -> + when (error) { + is InvoiceError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(error) + is UserNotLoggedInError -> invoiceDetailScreenEvents.askUserToLoginEvent.trigger() // user needs to login to continue + else -> { + invoiceListScreenEvents.invoiceErrorEvent.trigger( + InvoiceError( + ErrorWithCause(error.message ?: "Unknown error on invoice delete") + ) + ) + } + } + } + ).also { + refreshGetInvoices() + } } } - suspend fun deleteInvoice( - profileId: ProfileIdentifier, - taskId: String - ): PrescriptionServiceState = - deleteInvoiceFlow(profileId = profileId, taskId = taskId).cancellable().first() - - private fun deleteInvoiceFlow(profileId: ProfileIdentifier, taskId: String) = - flow { - emit(invoiceUseCase.deleteInvoice(profileId = profileId, taskId = taskId)) - }.map { result -> - result.fold( - onSuccess = { - State.InvoiceDeleted + fun downloadInvoices( + profileId: ProfileIdentifier = this.profileId + ) { + controllerScope.launch { + downloadInvoicesUseCase.invoke( + profileId = profileId, + onDownloadStarted = { + // do nothing, design decision to not show a loading indicator + } + ).collectResult( + onSuccess = { result -> + completeDownloadEvent() + when (result) { + is InvoiceError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(result) + // these are logged for debug purposes + is InvoiceResult.InvoiceSuccess -> Napier.i { "Invoices downloaded successfully" } + is InvoiceResult.InvoiceCombinedError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(InvoiceError(result.errorStates.first())) + else -> Napier.e { "wrong state reached on invoice download $result" } + } }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_NOT_FOUND, - HttpURLConnection.HTTP_GONE -> State.Error.InvoiceAlreadyDeleted - else -> throw it - } - } else { - throw it + onFailure = { error -> + completeDownloadEvent() + when (error) { + is UserNotLoggedInError -> invoiceDetailScreenEvents.askUserToLoginEvent.trigger() // user needs to login to continue + is InvoiceError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(error) + // What do we do when we have many errors on downloading the charge items? + is InvoiceResult.InvoiceCombinedError -> invoiceListScreenEvents.invoiceErrorEvent.trigger(InvoiceError(error.errorStates.first())) + else -> invoiceListScreenEvents.invoiceErrorEvent.trigger( + InvoiceError(ErrorWithCause(error.message ?: "Unknown error on invoice download")) + ) } } ) - }.retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) + } + } + + fun deleteLocalInvoices() { + controllerScope.launch { + invoices.first { it.isDataState }.data?.let { invoiceList -> + deleteAllInvoicesUseCase.invoke( + taskIds = invoiceList.values.flatten().map { it.taskId } + ).fold( + onSuccess = { + refreshGetInvoices() + }, + onFailure = { result -> + // state should not be reached + Napier.e { "wrong state reached on invoice delete $result" } + } + ) + } + } + } + + fun shareInvoice( + context: Context, + taskId: String, + fileProvider: FileProviderAuthority, + onCompletion: () -> Unit + ) { + controllerScope.launch { + getInvoiceForTaskId(taskId).first()?.let { + shareInvoiceUseCase.invoke( + context = context, + invoice = it, + fileProviderAuthority = fileProvider + ) + onCompletion() + } + } + } + + fun uiState( + consentState: ConsentState, + ssoTokenValid: Boolean, + invoice: InvoiceData.PKVInvoiceRecord? + ): InvoiceCardUiState { + return when { + consentState == ConsentState.ValidState.Loading && ssoTokenValid -> InvoiceCardUiState.Loading + !consentState.isConsentGranted() && consentState != ConsentState.ValidState.UnknownConsent -> InvoiceCardUiState.NoConsent + consentState.isConsentGranted() && invoice == null -> InvoiceCardUiState.NoInvoice + consentState.isConsentGranted() && invoice != null -> InvoiceCardUiState.ShowInvoice + else -> InvoiceCardUiState.Loading // Fallback state + } + } + + // TODO: Make different error messages for different error states + @Requirement( + "A_20085#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Error messages on refreshing invoices are localized" + ) + @Requirement( + "O.Plat_4#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "String resources are used tp show the mapped errors." + ) + fun invoiceErrorMessage(context: Context, errorState: HttpErrorState): String = + when (errorState) { + HttpErrorState.BadRequest, + HttpErrorState.Conflict, + HttpErrorState.Forbidden, + HttpErrorState.Gone, + HttpErrorState.MethodNotAllowed, + HttpErrorState.NotFound, + HttpErrorState.TooManyRequest, + HttpErrorState.Unauthorized, + HttpErrorState.RequestTimeout -> context.getString(R.string.error_message_vau_error) + + HttpErrorState.ServerError -> context.getString(R.string.error_message_server_communication_failed) + .format(errorState.errorCode) + + is ErrorWithCause, HttpErrorState.Unknown -> context.getString(R.string.error_message_network_not_available) + } } @Composable fun rememberInvoiceController(profileId: ProfileIdentifier): InvoiceController { - val invoiceUseCase by rememberInstance() + val biometricAuthenticator = LocalBiometricAuthenticator.current + + val getInvoicesByProfileUseCase by rememberInstance() + val getInvoiceByTaskIdUseCase by rememberInstance() + + val deleteInvoiceUseCase by rememberInstance() + val deleteAllInvoicesUseCase by rememberInstance() + + val downloadInvoicesUseCase by rememberInstance() + val shareInvoiceUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() - val authenticator = LocalAuthenticator.current - val fileProviderAuthority by rememberInstance() - val scope = rememberCoroutineScope() + val getActiveProfileUseCase by rememberInstance() + val getProfileByIdUseCase by rememberInstance() + + val chooseAuthenticationDataUseCase by rememberInstance() + return remember { InvoiceController( profileId = profileId, - invoiceUseCase = invoiceUseCase, + biometricAuthenticator = biometricAuthenticator, + downloadInvoicesUseCase = downloadInvoicesUseCase, getProfilesUseCase = getProfilesUseCase, - authenticator = authenticator, - fileProviderAuthority = fileProviderAuthority, - scope + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + shareInvoiceUseCase = shareInvoiceUseCase, + deleteInvoiceUseCase = deleteInvoiceUseCase, + deleteAllInvoicesUseCase = deleteAllInvoicesUseCase, + getInvoiceByTaskIdUseCase = getInvoiceByTaskIdUseCase, + getInvoicesByProfileUseCase = getInvoicesByProfileUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase ) } } - -fun refreshInvoiceErrorMessage(context: Context, errorState: PrescriptionServiceErrorState): String? = - when (errorState) { - GeneralErrorState.NetworkNotAvailable -> - context.getString(R.string.error_message_network_not_available) - - is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - context.getString(R.string.error_message_server_communication_failed).format(errorState.code) - - GeneralErrorState.FatalTruststoreState -> - context.getString(R.string.error_message_vau_error) - - else -> null - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceDetailScreenEvents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceDetailScreenEvents.kt new file mode 100644 index 00000000..4c2d9d3c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceDetailScreenEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.presentation + +import de.gematik.ti.erp.app.utils.compose.ComposableEvent + +data class InvoiceDetailScreenEvents( + val askUserToLoginEvent: ComposableEvent = ComposableEvent(), + val deleteSuccessfulEvent: ComposableEvent = ComposableEvent() +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceListScreenEvents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceListScreenEvents.kt new file mode 100644 index 00000000..8f698a4b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceListScreenEvents.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.presentation + +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceError +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.utils.compose.ComposableEvent + +data class InvoiceListScreenEvents( + val downloadCompletedEvent: ComposableEvent = ComposableEvent(), + val invoiceErrorEvent: ComposableEvent = ComposableEvent(), + val getConsentEvent: ComposableEvent = ComposableEvent(), + val showAuthenticationErrorDialog: ComposableEvent = ComposableEvent() +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/model/InvoiceCardUiState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/model/InvoiceCardUiState.kt new file mode 100644 index 00000000..ec94163e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/presentation/model/InvoiceCardUiState.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.presentation.model + +sealed interface InvoiceCardUiState { + data object Loading : InvoiceCardUiState + data object NoConsent : InvoiceCardUiState + data object NoInvoice : InvoiceCardUiState + data object ShowInvoice : InvoiceCardUiState +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentMessages.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentMessages.kt deleted file mode 100644 index 873fc633..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentMessages.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource -import com.google.android.material.snackbar.Snackbar -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pkv.presentation.ConsentController -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.extensions.SnackbarScaffold - -@Composable -fun HandleConsentState( - consentState: PrescriptionServiceState, - snackbar: SnackbarScaffold, - onClickSnackbarAction: () -> Unit -) { - val consentGrantedInfo = stringResource(R.string.consent_granted_info) - val actionId = R.string.consent_action_to_invoices - - when (consentState) { - ConsentController.ConsentState.Granted -> { - snackbar.show( - consentGrantedInfo, - length = Snackbar.LENGTH_LONG, - actionTextId = actionId, - onClickAction = onClickSnackbarAction - ) - } - - else -> { - // Don't show anything - } - } -} - -@Composable -fun HandleConsentErrorState( - consentErrorState: PrescriptionServiceState, - onShowCardWall: () -> Unit, - onClickToInvoices: () -> Unit, - onRetry: () -> Unit -) { - var showAlertDialog by remember { mutableStateOf(true) } - val alertMessage = consentErrorMessage(consentErrorState) - val onCancel = { showAlertDialog = false } - - alertMessage?.let { message -> - if (showAlertDialog) { - when (consentErrorState) { - ConsentController.ConsentErrorState.AlreadyGranted -> ConsentErrorDialog( - message, - onClickToInvoices, - onCancel - ) - ConsentController.ConsentErrorState.NoInternet -> ConsentErrorDialog(message, onRetry, onCancel) - ConsentController.ConsentErrorState.BadRequest -> ConsentErrorDialog(message, {}, onCancel) - ConsentController.ConsentErrorState.ServerTimeout -> ConsentErrorDialog(message, onRetry, onCancel) - ConsentController.ConsentErrorState.InternalError -> ConsentErrorDialog(message, onRetry, onCancel) - ConsentController.ConsentErrorState.TooManyRequests -> ConsentErrorDialog(message, onRetry, onCancel) - ConsentController.ConsentErrorState.Forbidden -> ConsentErrorDialog(message, {}, onCancel) - ConsentController.ConsentErrorState.Unauthorized -> ConsentErrorDialog( - message, - onShowCardWall, - onCancel - ) - else -> { - // Don't show anything - } - } - } - } -} - -data class ConsentAlertData( - val header: String, - val info: String, - val cancelText: String, - val actionText: String? = null -) - -@Composable -fun consentErrorMessage( - errorState: PrescriptionServiceState -): ConsentAlertData? { - val cancelText = stringResource(R.string.consent_error_dialog_cancel) - val retryText = stringResource(R.string.consent_error_dialog_retry) - return when (errorState) { - ConsentController.ConsentErrorState.AlreadyGranted -> ConsentAlertData( - header = stringResource(R.string.consent_error_already_granted_header), - info = stringResource(R.string.consent_error_already_granted_info), - cancelText = cancelText, - actionText = stringResource(R.string.consent_action_to_invoices) - ) - ConsentController.ConsentErrorState.NoInternet -> ConsentAlertData( - header = stringResource(R.string.consent_error_no_internet_header), - info = stringResource(R.string.consent_error_no_internet_info), - cancelText = cancelText, - actionText = retryText - ) - ConsentController.ConsentErrorState.BadRequest -> ConsentAlertData( - header = stringResource(R.string.consent_error_bad_request_header), - info = stringResource(R.string.consent_error_bad_request_info), - cancelText = cancelText - ) - ConsentController.ConsentErrorState.ServerTimeout -> ConsentAlertData( - header = stringResource(R.string.consent_error_server_timeout_header), - info = stringResource(R.string.consent_error_server_timeout_info), - cancelText = cancelText, - actionText = retryText - ) - ConsentController.ConsentErrorState.InternalError -> ConsentAlertData( - header = stringResource(R.string.consent_error_internal_error_header), - info = stringResource(R.string.consent_error_internal_error_info), - cancelText = cancelText, - actionText = retryText - ) - ConsentController.ConsentErrorState.TooManyRequests -> ConsentAlertData( - header = stringResource(R.string.consent_error_too_many_requests_header), - info = stringResource(R.string.consent_error_too_many_requests_info), - cancelText = cancelText, - actionText = retryText - ) - ConsentController.ConsentErrorState.Forbidden -> ConsentAlertData( - header = stringResource(R.string.consent_error_forbidden_header), - info = stringResource(R.string.consent_error_forbidden_info), - cancelText = cancelText - ) - ConsentController.ConsentErrorState.Unauthorized -> ConsentAlertData( - header = stringResource(R.string.consent_error_unauthorized_header), - info = stringResource(R.string.consent_error_unauthorized_info), - cancelText = cancelText, - actionText = stringResource(R.string.consent_error_connect) - ) - else -> null - } -} - -@Composable -fun ConsentErrorDialog(alertData: ConsentAlertData, onAction: () -> Unit, onCancel: () -> Unit) = - if (alertData.actionText != null) { - CommonAlertDialog( - header = alertData.header, - info = alertData.info, - actionText = alertData.actionText, - cancelText = alertData.cancelText, - onClickAction = onAction, - onCancel = onCancel - ) - } else { - AcceptDialog( - header = alertData.header, - info = alertData.info, - acceptText = alertData.cancelText, - onClickAccept = onCancel - ) - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/DeprecatedConsentController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/DeprecatedConsentController.kt deleted file mode 100644 index c2c5c166..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/DeprecatedConsentController.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator -import de.gematik.ti.erp.app.consent.usecase.GetConsentUseCase -import de.gematik.ti.erp.app.consent.usecase.GrantConsentUseCase -import de.gematik.ti.erp.app.consent.usecase.RevokeConsentUseCase -import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions -import de.gematik.ti.erp.app.prescription.presentation.retryWithAuthenticator -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import org.kodein.di.compose.rememberInstance -import java.net.HttpURLConnection - -@Deprecated( - "This is bad code which is kept", - replaceWith = ReplaceWith("de.gematik.ti.erp.app.pkv.presentation.ConsentController"), - level = DeprecationLevel.WARNING -) -@Stable -class DeprecatedConsentController( - val context: Context, - val profileId: ProfileIdentifier, - val insuranceIdentifier: String, - private val authenticator: Authenticator, - private val getConsentUseCase: GetConsentUseCase, - private val grantConsentUseCase: GrantConsentUseCase, - private val revokeConsentUseCase: RevokeConsentUseCase -) { - - sealed interface State : PrescriptionServiceState { - object ChargeConsentNotGranted : State - object ChargeConsentGranted : State - object ChargeConsentRevoked : State - - sealed interface Error : State, PrescriptionServiceErrorState { - object ChargeConsentAlreadyGranted : Error - object ChargeConsentAlreadyRevoked : Error - } - fun PrescriptionServiceState.isConsentGranted() = - this == ChargeConsentGranted || this == Error.ChargeConsentAlreadyGranted - } - - fun getChargeConsent() = flow { - emit(getConsentUseCase(profileId)) - }.map { result -> - result.map { - when (it) { - true -> State.ChargeConsentGranted - else -> State.ChargeConsentNotGranted - } - }.getOrThrow() - }.retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) - - fun grantChargeConsent() = flow { - emit(grantConsentUseCase(profileId, insuranceIdentifier)) - }.map { result -> - result.fold( - onSuccess = { - State.ChargeConsentGranted - }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_CONFLICT -> State.Error.ChargeConsentAlreadyGranted - else -> throw it - } - } else { - throw it - } - } - ) - }.retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) - - fun revokeChargeConsent() = flow { - emit(revokeConsentUseCase(profileId)) - }.map { result -> - result.fold( - onSuccess = { - State.ChargeConsentRevoked - }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_NOT_FOUND -> State.Error.ChargeConsentAlreadyRevoked - else -> throw it - } - } else { - throw it - } - } - ) - }.retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) -} - -@Deprecated( - "This is bad code which is kept", - replaceWith = ReplaceWith("de.gematik.ti.erp.app.pkv.presentation.ConsentController"), - level = DeprecationLevel.WARNING -) -@Composable -fun rememberDeprecatedConsentController(profile: ProfilesUseCaseData.Profile): DeprecatedConsentController { - val context = LocalContext.current - val authenticator = LocalAuthenticator.current - - val getConsentUseCase by rememberInstance() - val grantConsentUseCase by rememberInstance() - val revokeConsentUseCase by rememberInstance() - - return remember(profile.id, profile.insurance.insuranceIdentifier) { - DeprecatedConsentController( - context = context, - profileId = profile.id, - insuranceIdentifier = profile.insurance.insuranceIdentifier, - authenticator = authenticator, - getConsentUseCase = getConsentUseCase, - grantConsentUseCase = grantConsentUseCase, - revokeConsentUseCase = revokeConsentUseCase - ) - } -} - -fun consentErrorMessage(context: Context, consentErrorState: PrescriptionServiceErrorState): String? = - when (consentErrorState) { - GeneralErrorState.NetworkNotAvailable -> - context.getString(R.string.error_message_network_not_available) - is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - context.getString(R.string.error_message_server_communication_failed).format(consentErrorState.code) - GeneralErrorState.FatalTruststoreState -> - context.getString(R.string.error_message_vau_error) - else -> null - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt deleted file mode 100644 index 6629325c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate.joinMedicationInfo -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.LabeledText -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.TertiaryButton -import de.gematik.ti.erp.app.utils.compose.visualTestTag - -class InvoiceDetailsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val pkvNavigationArguments = remember { - val arguments = requireNotNull(navBackStackEntry.arguments) - PkvNavigationArguments( - taskId = requireNotNull(arguments.getString(PkvRoutes.TaskId)), - profileId = requireNotNull(arguments.getString(PkvRoutes.ProfileId)) - ) - } - val invoiceController = rememberInvoiceController(pkvNavigationArguments.profileId) - val profiles by invoiceController.getProfilesState() - profiles.profileById(pkvNavigationArguments.profileId)?.let { selectedProfile -> - val listState = rememberLazyListState() - val scaffoldState = rememberScaffoldState() - val scope = rememberCoroutineScope() - val context = LocalContext.current - val invoice by produceState(null) { - invoiceController.detailState(pkvNavigationArguments.taskId).collect { - value = it - } - } - var showDeleteInvoiceAlert by remember { mutableStateOf(false) } - - if (showDeleteInvoiceAlert) { - DeleteInvoiceDialog( - onCancel = { - showDeleteInvoiceAlert = false - } - ) { - onDeleteInvoice( - scope, - pkvNavigationArguments.taskId, - invoiceController, - selectedProfile, - context, - scaffoldState - ) { - showDeleteInvoiceAlert = false - navController.popBackStack() - } - } - } - - AnimatedElevationScaffold( - modifier = Modifier - .imePadding() - .visualTestTag(TestTag.Profile.InvoicesDetailScreen), - topBarTitle = "", - navigationMode = NavigationBarMode.Back, - scaffoldState = scaffoldState, - bottomBar = { - invoice?.let { - InvoiceDetailBottomBar( - it.invoice.totalBruttoAmount, - onClickSubmit = { - navController.navigate( - PkvRoutes.InvoiceShareScreen.path( - taskId = it.taskId, - profileId = selectedProfile.id - ) - ) - } - ) - } - }, - listState = listState, - actions = { - Row { - invoice?.let { invoice -> - InvoiceThreeDotMenu( - invoice.taskId, - onClickShareInvoice = { - navController.navigate( - PkvRoutes.InvoiceShareScreen.path( - taskId = invoice.taskId, - profileId = selectedProfile.id - ) - ) - }, - onClickRemoveInvoice = { showDeleteInvoiceAlert = true }, - onClickCorrectInvoiceLocally = { - navController.navigate( - PkvRoutes.InvoiceLocalCorrectionScreen.path( - taskId = invoice.taskId, - profileId = selectedProfile.id - ) - ) - } - ) - } - } - }, - onBack = { navController.popBackStack() } - ) { innerPadding -> - InvoiceDetailsScreenContent( - navController, - innerPadding, - listState, - invoice, - selectedProfile - ) - } - } - } -} - -@Composable -private fun InvoiceDetailsScreenContent( - navController: NavController, - innerPadding: PaddingValues, - listState: LazyListState, - invoice: InvoiceData.PKVInvoice?, - selectedProfile: ProfilesUseCaseData.Profile -) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding( - top = PaddingDefaults.Medium + innerPadding.calculateTopPadding(), - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ), - state = listState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) - ) { - invoice?.let { - item { - InvoiceMedicationHeader(it) - } - item { - LabeledText( - description = stringResource(R.string.invoice_prescribed_by), - content = it.practitioner.name - ) - } - item { - LabeledText( - description = stringResource(R.string.invoice_redeemed_in), - content = it.pharmacyOrganization.name - ) - } - item { - LabeledText( - description = stringResource(R.string.invoice_redeemed_on), - content = it.whenHandedOver?.formattedString() - ) - } - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - TertiaryButton(onClick = { - navController.navigate( - PkvRoutes.InvoiceExpandedDetailsScreen.path( - taskId = it.taskId, - profileId = selectedProfile.id - ) - ) - }) { - Text(text = stringResource(R.string.invoice_show_more)) - } - } - } - } - item { - if ( - (navController.previousBackStackEntry?.id) - == PrescriptionDetailRoutes.PrescriptionDetailScreen.route - ) { - LinkToInvoiceList(navController, profileIdentifier = selectedProfile.id) - } - } - } -} - -@Composable -private fun InvoiceDetailBottomBar(totalBruttoAmount: Double, onClickSubmit: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .background( - color = AppTheme.colors.neutral100 - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.padding(PaddingDefaults.Medium)) { - Text( - stringResource(R.string.invoice_details_cost, totalBruttoAmount), - style = AppTheme.typography.h6, - fontWeight = FontWeight.Bold - ) - Text(stringResource(R.string.invoice_detail_total_brutto_amount), style = AppTheme.typography.body2l) - } - - Button( - onClick = onClickSubmit, - modifier = Modifier.padding(end = PaddingDefaults.Medium) - ) { - Text(text = stringResource(R.string.invoice_details_submit)) - } - } -} - -@Composable -fun InvoiceMedicationHeader(invoice: InvoiceData.PKVInvoice) { - val medicationInfo = joinMedicationInfo(invoice.medicationRequest) - Text(text = medicationInfo, style = AppTheme.typography.h5) -} - -@Composable -private fun LinkToInvoiceList(navController: NavController, profileIdentifier: ProfileIdentifier) { - Row( - modifier = Modifier - .clickable { navController.navigate(PkvRoutes.InvoiceListScreen.path(profileId = profileIdentifier)) } - .padding(PaddingDefaults.Medium) - .wrapContentWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.link_to_invoice_list), - style = AppTheme.typography.body2, - color = AppTheme.colors.primary600 - ) - SpacerTiny() - Icon(Icons.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.primary600) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt deleted file mode 100644 index 57729515..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.toAnnotatedString - -@Composable -fun RevokeConsentDialog(onCancel: () -> Unit, onRevokeConsent: () -> Unit) { - CommonAlertDialog( - header = stringResource(R.string.profile_revoke_consent_header), - info = stringResource(R.string.profile_revoke_consent_info), - cancelText = stringResource(R.string.profile_invoices_cancel), - actionText = stringResource(R.string.profile_revoke_consent), - cancelTextColor = AppTheme.colors.primary600, - actionTextColor = AppTheme.colors.red600, - onCancel = onCancel, - onClickAction = onRevokeConsent - ) -} - -@Composable -fun DeleteInvoiceDialog(onCancel: () -> Unit, onDeleteInvoice: () -> Unit) { - CommonAlertDialog( - header = stringResource(R.string.profile_delete_invoice_header), - info = stringResource(R.string.profile_delete_invoice_info), - cancelText = stringResource(R.string.profile_invoices_cancel), - actionText = stringResource(R.string.profile_delete_invoice), - cancelTextColor = AppTheme.colors.primary600, - actionTextColor = AppTheme.colors.red600, - onCancel = onCancel, - onClickAction = onDeleteInvoice - ) -} - -@Composable -fun GrantConsentDialog(onCancel: () -> Unit, onGrantConsent: () -> Unit) { - CommonAlertDialog( - header = stringResource(R.string.get_consent_info_dialog_header).toAnnotatedString(), - info = stringResource(R.string.get_consent_info_dialog_info).toAnnotatedString(), - cancelText = stringResource(R.string.get_consent_info_dialog_cancel), - actionText = stringResource(R.string.get_consent_info_dialog_accept), - onCancel = onCancel, - onClickAction = onGrantConsent - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceExpandedDetailsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceExpandedDetailsScreen.kt deleted file mode 100644 index a8baf0e6..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceExpandedDetailsScreen.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.model.currencyString -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.LabeledText -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.visualTestTag - -class InvoiceExpandedDetailsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val pkvNavigationArguments = remember { - val arguments = requireNotNull(navBackStackEntry.arguments) - PkvNavigationArguments( - taskId = requireNotNull(arguments.getString(PkvRoutes.TaskId)), - profileId = requireNotNull(arguments.getString(PkvRoutes.ProfileId)) - ) - } - val invoiceController = rememberInvoiceController(pkvNavigationArguments.profileId) - val listState = rememberLazyListState() - val scaffoldState = rememberScaffoldState() - val invoice by produceState(null) { - invoiceController.detailState(pkvNavigationArguments.taskId).collect { - value = it - } - } - AnimatedElevationScaffold( - modifier = Modifier - .imePadding() - .visualTestTag(TestTag.Profile.InvoicesDetailScreen), - topBarTitle = "", - navigationMode = NavigationBarMode.Back, - scaffoldState = scaffoldState, - listState = listState, - actions = {}, - onBack = { navController.popBackStack() } - ) { innerPadding -> - InvoiceExpandedDetailsScreenContent( - innerPadding, - listState, - invoice - ) - } - } -} - -@Composable -private fun InvoiceExpandedDetailsScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - invoice: InvoiceData.PKVInvoice? -) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding( - top = PaddingDefaults.Medium + innerPadding.calculateTopPadding(), - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ), - state = listState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) - ) { - invoice?.let { - item { - InvoiceMedicationHeader(it) - } - item { - LabeledText(description = stringResource(R.string.invoice_task_id), content = it.taskId) - } - item { - PatientLabel(it.patient) - } - item { - PractitionerLabel(it.practitioner, it.practitionerOrganization) - } - item { - PharmacyLabel(it.pharmacyOrganization) - } - item { - LabeledText( - description = stringResource(R.string.invoice_redeemed_on), - content = it.whenHandedOver?.formattedString() - ) - } - item { - PriceData(it.invoice) - } - } - } -} - -@Composable -private fun PriceData(invoice: InvoiceData.Invoice) { - val (fees, articles) = invoice.chargeableItems.partition { - (it.description as? InvoiceData.ChargeableItem.Description.PZN)?.isSpecialPZN() ?: false - } - - articles.map { - val article = when (it.description) { - is InvoiceData.ChargeableItem.Description.HMNR -> - stringResource( - R.string.invoice_description_hmknr, - (it.description as InvoiceData.ChargeableItem.Description.HMNR).hmnr - ) - is InvoiceData.ChargeableItem.Description.PZN -> - stringResource( - R.string.invoice_description_pzn, - (it.description as InvoiceData.ChargeableItem.Description.PZN).pzn - ) - is InvoiceData.ChargeableItem.Description.TA1 -> - stringResource( - R.string.invoice_description_ta1, - (it.description as InvoiceData.ChargeableItem.Description.TA1).ta1 - ) - } - - Text(stringResource(R.string.invoice_description_articel, article)) - Text(stringResource(R.string.invoice_description_factor, it.factor)) - Text(stringResource(R.string.invoice_description_tax, it.price.tax.currencyString())) - Text(stringResource(R.string.invoice_description_brutto_price, it.price.value)) - - SpacerMedium() - } - - if (fees.isNotEmpty()) { - Text(stringResource(R.string.invoice_description_additional_fees)) - fees.map { - require(it.description is InvoiceData.ChargeableItem.Description.PZN) - val article = when ( - InvoiceData.SpecialPZN.valueOfPZN( - (it.description as InvoiceData.ChargeableItem.Description.PZN).pzn - ) - ) { - InvoiceData.SpecialPZN.EmergencyServiceFee -> stringResource(R.string.invoice_details_emergency_fee) - InvoiceData.SpecialPZN.BTMFee -> stringResource(R.string.invoice_details_narcotic_fee) - InvoiceData.SpecialPZN.TPrescriptionFee -> stringResource(R.string.invoice_details_t_prescription_fee) - InvoiceData.SpecialPZN.ProvisioningCosts -> stringResource(R.string.invoice_details_provisioning_costs) - InvoiceData.SpecialPZN.DeliveryServiceCosts -> - stringResource(R.string.invoice_details_delivery_service_costs) - null -> error("wrong mapping") - } - - Text(stringResource(R.string.invoice_description_articel, article)) - Text(stringResource(R.string.invoice_description_brutto_price, it.price.value)) - - SpacerMedium() - } - } - - Text(stringResource(R.string.invoice_description_total_brutto_amount, invoice.totalBruttoAmount)) - - Text(stringResource(R.string.invoice_detail_dispense), style = AppTheme.typography.body2l) - SpacerXXLarge() -} - -@Composable -private fun PharmacyLabel(pharmacyOrganization: SyncedTaskData.Organization) { - LabeledTextItems( - label = stringResource(R.string.invoice_redeemed_in), - items = listOf( - pharmacyOrganization.name, - pharmacyOrganization.address?.joinToString(), - pharmacyOrganization.uniqueIdentifier?.let { stringResource(R.string.invoice_pharmacy_id, it) } - ) - ) -} - -@Composable -private fun PractitionerLabel( - practitioner: SyncedTaskData.Practitioner, - practitionerOrganization: SyncedTaskData.Organization -) { - LabeledTextItems( - label = stringResource(R.string.invoice_prescribed_by), - items = listOf( - practitioner.name, - practitionerOrganization.address?.joinToString(), - practitioner.practitionerIdentifier?.let { stringResource(R.string.invoice_practitioner_id, it) } - ) - ) -} - -@Composable -private fun LabeledTextItems(label: String, items: List) { - val showLabel = items.any { it != null } - items.forEach { - it?.let { - Text(it, style = AppTheme.typography.body1) - } - } - if (showLabel) { - Text(label, style = AppTheme.typography.body2l) - } -} - -@Composable -private fun PatientLabel(patient: SyncedTaskData.Patient) { - LabeledTextItems( - label = stringResource(R.string.invoice_prescribed_for), - items = listOf( - patient.name, - patient.insuranceIdentifier?.let { stringResource(R.string.invoice_insurance_id, it) }, - patient.address?.joinToString(), - patient.birthdate?.formattedString()?.let { stringResource(R.string.invoice_born_on, it) } - ) - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceListScreen.kt deleted file mode 100644 index dddedb13..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceListScreen.kt +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import android.content.Context -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ScaffoldState -import androidx.compose.material.SnackbarHost -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowRight -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.model.currencyString -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.InvoiceController -import de.gematik.ti.erp.app.pkv.presentation.refreshInvoiceErrorMessage -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController -import de.gematik.ti.erp.app.pkv.ui.DeprecatedConsentController.State.ChargeConsentNotGranted.isConsentGranted -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.presentation.rememberRefreshPrescriptionsController -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ConnectBottomBar -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXLarge -import de.gematik.ti.erp.app.utils.compose.visualTestTag -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.datetime.TimeZone.Companion.currentSystemDefault -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter - -class InvoiceListScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Suppress("LongMethod") - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(PkvRoutes.ProfileId)) } - val invoiceController = rememberInvoiceController(profileId) - val profiles by invoiceController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val listState = rememberLazyListState() - val scaffoldState = rememberScaffoldState() - val scope = rememberCoroutineScope() - val consentController = rememberDeprecatedConsentController(profile = selectedProfile) - var consentGranted by remember { mutableStateOf(false) } - var showGrantConsentDialog by remember { mutableStateOf(false) } - val context = LocalContext.current - - val ssoTokenValid = rememberSaveable(selectedProfile.ssoTokenScope) { - selectedProfile.isSSOTokenValid() - } - - CheckConsentState(consentController, ssoTokenValid, scaffoldState, context) { - consentGranted = it - showGrantConsentDialog = !consentGranted - } - - var showRevokeConsentAlert by remember { mutableStateOf(false) } - - if (showGrantConsentDialog && selectedProfile.isSSOTokenValid()) { - GrantConsentDialog( - onCancel = { navController.popBackStack() } - ) { - onGrantConsent(context, scope, consentController, scaffoldState) { - consentGranted = it - showGrantConsentDialog = false - } - } - } - - if (showRevokeConsentAlert) { - RevokeConsentDialog( - onCancel = { showRevokeConsentAlert = false } - ) { - onRevokeConsent(context, scope, consentController, scaffoldState, { consentGranted = it }) { - navController.popBackStack() - } - } - } - - var showDeleteInvoiceAlert by remember { mutableStateOf(false) } - var invoiceToDeleteTaskID: String? by remember { mutableStateOf(null) } - - if (showDeleteInvoiceAlert) { - DeleteInvoiceDialog( - onCancel = { - showDeleteInvoiceAlert = false - invoiceToDeleteTaskID = null - } - ) { - onDeleteInvoice( - scope, - invoiceToDeleteTaskID, - invoiceController, - selectedProfile, - context, - scaffoldState - ) { - showDeleteInvoiceAlert = false - invoiceToDeleteTaskID = null - } - } - } - - val mainScreenController = rememberMainScreenController() - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) - - AnimatedElevationScaffold( - modifier = Modifier - .imePadding() - .visualTestTag(TestTag.Profile.InvoicesScreen), - topBarTitle = stringResource(R.string.profile_invoices), - snackbarHost = { - SnackbarHost(it, modifier = Modifier.systemBarsPadding()) - }, - bottomBar = { - if (!ssoTokenValid) { - ConnectBottomBar( - infoText = stringResource(R.string.invoices_connect_info) - ) { - refreshPrescriptionsController.refresh( - profileId = selectedProfile.id, - isUserAction = true, - onUserNotAuthenticated = {}, - onShowCardWall = { - navController.navigate( - CardWallRoutes.CardWallIntroScreen.path(selectedProfile.id) - ) - } - ) - } - } - }, - navigationMode = NavigationBarMode.Back, - scaffoldState = scaffoldState, - listState = listState, - actions = { - Row { - InvoicesHeaderThreeDotMenu( - consentGranted = remember(consentGranted) { consentGranted }, - onClickRevokeConsent = { showRevokeConsentAlert = true } - ) - } - }, - onBack = { navController.popBackStack() } - ) { - RefreshInvoicesContent( - profileIdentifier = selectedProfile.id, - invoiceController = invoiceController, - ssoTokenValid = ssoTokenValid, - listState = listState, - consentGranted = consentGranted, - onRefreshInvoicesError = { - scope.launch { - scaffoldState.snackbarHostState.showSnackbar(it) - } - }, - onClickInvoice = { taskId -> - navController.navigate( - PkvRoutes.InvoiceDetailsScreen.path(taskId = taskId, profileId = selectedProfile.id) - ) - } - ) - } - } - } -} - -@Composable -fun CheckConsentState( - consentController: DeprecatedConsentController, - ssoTokenValid: Boolean, - scaffoldState: ScaffoldState, - context: Context, - saveConsentState: (Boolean) -> Unit -) { - LaunchedEffect(Unit) { - if (ssoTokenValid) { - val consentState = consentController.getChargeConsent().first() - when (consentState) { - is PrescriptionServiceErrorState -> { - consentErrorMessage(context, consentState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - val isGranted = consentState.isConsentGranted() - saveConsentState(isGranted) - } - } -} - -fun onGrantConsent( - context: Context, - scope: CoroutineScope, - consentController: DeprecatedConsentController, - scaffoldState: ScaffoldState, - saveConsentState: (Boolean) -> Unit -) { - scope.launch { - val consentState = consentController.grantChargeConsent().first() - when (consentState) { - is PrescriptionServiceErrorState -> { - consentErrorMessage(context, consentState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - saveConsentState(consentState.isConsentGranted()) - } -} - -private fun onRevokeConsent( - context: Context, - scope: CoroutineScope, - consentController: DeprecatedConsentController, - scaffoldState: ScaffoldState, - saveConsentState: (Boolean) -> Unit, - onBack: () -> Unit -) { - scope.launch { - val consentState = consentController.revokeChargeConsent().first() - when (consentState) { - is PrescriptionServiceErrorState -> { - consentErrorMessage(context, consentState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - saveConsentState(consentState.isConsentGranted()) - onBack() - } -} -fun onDeleteInvoice( - scope: CoroutineScope, - invoiceToDeleteTaskID: String?, - invoiceController: InvoiceController, - selectedProfile: ProfilesUseCaseData.Profile, - context: Context, - scaffoldState: ScaffoldState, - finally: () -> Unit -) { - scope.launch { - val invoiceState = - invoiceToDeleteTaskID?.let { - invoiceController.deleteInvoice( - profileId = selectedProfile.id, - taskId = it - ) - } - when (invoiceState) { - is PrescriptionServiceErrorState -> { - refreshInvoiceErrorMessage(context, invoiceState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - finally() - } -} - -@Composable -@OptIn(ExperimentalMaterialApi::class) -private fun RefreshInvoicesContent( - profileIdentifier: ProfileIdentifier, - invoiceController: InvoiceController, - listState: LazyListState, - ssoTokenValid: Boolean, - consentGranted: Boolean, - onRefreshInvoicesError: (String) -> Unit, - onClickInvoice: (String) -> Unit -) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - - val isRefreshing by invoiceController.isRefreshing - - fun refresh() = scope.launch { - when (val state = invoiceController.downloadInvoices(profileIdentifier).first()) { - is PrescriptionServiceErrorState -> { - refreshInvoiceErrorMessage(context, state)?.let { - onRefreshInvoicesError(it) - } - } - } - } - - val refreshState = rememberPullRefreshState(isRefreshing, ::refresh) - - LaunchedEffect(ssoTokenValid, consentGranted) { - if (ssoTokenValid && consentGranted) { - refresh() - } - } - - Box( - Modifier - .fillMaxSize() - .pullRefresh(refreshState, enabled = ssoTokenValid && consentGranted) - ) { - PullRefreshIndicator( - refreshing = isRefreshing, - state = refreshState, - modifier = Modifier.align(Alignment.TopCenter), - contentColor = AppTheme.colors.primary600, - scale = true - ) - Invoices( - listState = listState, - invoiceController = invoiceController, - onClickInvoice = onClickInvoice - ) - } -} - -@Composable -private fun InvoicesHeaderThreeDotMenu(consentGranted: Boolean, onClickRevokeConsent: () -> Unit) { - var expanded by remember { mutableStateOf(false) } - - IconButton( - onClick = { expanded = true } - ) { - Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - offset = DpOffset(24.dp, 0.dp) - ) { - DropdownMenuItem( - onClick = { - onClickRevokeConsent() - expanded = false - }, - enabled = consentGranted - ) { - Text( - text = stringResource(R.string.profile_revoke_consent), - color = if (consentGranted) { - AppTheme.colors.red600 - } else { - AppTheme.colors.neutral900 - } - ) - } - } -} - -@Composable -private fun Invoices( - listState: LazyListState, - invoiceController: InvoiceController, - onClickInvoice: (String) -> Unit -) { - val invoiceState by invoiceController.state - - LazyColumn( - modifier = Modifier - .fillMaxSize(), - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues(), - state = listState - ) { - invoiceState?.let { state -> - if (state.entries.isEmpty()) { - item { - InvoicesEmptyScreen() - } - } else { - state.entries.forEach { entry -> - item { - val formattedYear = remember { - val dateFormatter = DateTimeFormatter.ofPattern("yyyy") - if (entry.value.isNotEmpty()) { - entry.value[0].timestamp - .toLocalDateTime(currentSystemDefault()) - .toJavaLocalDateTime().format(dateFormatter) - } else { - "" - } - } - - val totalSumOfInvoices = entry.value.sumOf { - it.invoice.totalBruttoAmount - }.currencyString() - SpacerMedium() - HeadingPerYear(formattedYear, totalSumOfInvoices, entry.value[0].invoice.currency) - } - entry.value.forEach { invoice -> - item { - val formattedDate = remember { - val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") - invoice.timestamp - .toLocalDateTime(currentSystemDefault()) - .toJavaLocalDateTime().format(dateFormatter) - } - Invoice( - invoice = invoice, - formattedDate = formattedDate, - onClickInvoice = onClickInvoice - ) - Divider(modifier = Modifier.padding(start = PaddingDefaults.Medium)) - } - } - item { SpacerXLarge() } - } - } - } - } -} - -@Composable -private fun Invoice( - invoice: InvoiceData.PKVInvoice, - formattedDate: String, - onClickInvoice: (String) -> Unit -) { - Napier.d { "invoice invoice.taskid ${invoice.taskId}" } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onClickInvoice(invoice.taskId) - }, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium - ).weight(1f) - - ) { - val itemName = invoice.medicationRequest.medication?.name() ?: "" - - Text( - itemName, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - style = AppTheme.typography.subtitle1, - color = AppTheme.colors.neutral900 - ) - SpacerTiny() - Text( - formattedDate, - overflow = TextOverflow.Ellipsis, - style = AppTheme.typography.body2l - ) - SpacerSmall() - TotalBruttoAmountChip( - text = invoice.invoice.totalBruttoAmount.currencyString() + " " + invoice.invoice.currency - ) - } - Row(modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), horizontalArrangement = Arrangement.End) { - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } - } -} - -@Composable -private fun TotalBruttoAmountChip( - text: String, - modifier: Modifier = Modifier -) { - val shape = RoundedCornerShape(8.dp) - - Row( - Modifier - .background(AppTheme.colors.neutral025, shape) - .border(1.dp, AppTheme.colors.neutral300, shape) - .clip(shape) - .then(modifier) - .padding(vertical = 6.dp, horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text, style = AppTheme.typography.subtitle2, color = AppTheme.colors.neutral900) - } -} - -@Composable -private fun HeadingPerYear( - formattedYear: String, - totalSumOfInvoices: String, - currency: String -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = formattedYear, - style = AppTheme.typography.h6 - ) - Text( - text = stringResource( - R.string.pkv_invoices_total_of_year, - totalSumOfInvoices, - currency - ), - style = AppTheme.typography.subtitle2, - color = AppTheme.colors.primary600 - ) - } -} - -@Composable -private fun LazyItemScope.InvoicesEmptyScreen() { - Column( - modifier = Modifier - .fillParentMaxSize() - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Image( - painterResource(R.drawable.girl_red_oh_no), - contentDescription = null - ) - Text( - stringResource(R.string.invoices_no_invoices), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.offset(y = -(PaddingDefaults.Large)) - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt deleted file mode 100644 index f865bb29..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import com.google.zxing.common.BitMatrix -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.DataMatrix -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.createBitMatrix -import de.gematik.ti.erp.app.utils.extensions.forceBrightness - -class InvoiceLocalCorrectionScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val pkvNavigationArguments = remember { - val arguments = requireNotNull(navBackStackEntry.arguments) - PkvNavigationArguments( - taskId = requireNotNull(arguments.getString(PkvRoutes.TaskId)), - profileId = requireNotNull(arguments.getString(PkvRoutes.ProfileId)) - ) - } - val invoiceController = rememberInvoiceController(pkvNavigationArguments.profileId) - val listState = rememberLazyListState() - val scaffoldState = rememberScaffoldState() - val invoice by produceState(null) { - invoiceController.detailState(pkvNavigationArguments.taskId).collect { - value = it - } - } - val matrix = remember(invoice) { - invoice?.dmcPayload?.let { - createBitMatrix(it) - } - } - val activity = LocalActivity.current - activity.forceBrightness() - AnimatedElevationScaffold( - modifier = Modifier - .imePadding(), - topBarTitle = "", - navigationMode = null, - scaffoldState = scaffoldState, - listState = listState, - actions = { - TextButton( - onClick = { navController.popBackStack() } - ) { - Text(stringResource(R.string.invoice_correct_done)) - } - }, - onBack = { navController.popBackStack() } - ) { innerPadding -> - InvoiceLocalCorrectionScreenContent( - innerPadding, - listState, - invoice, - matrix - ) - } - } -} - -@Composable -private fun InvoiceLocalCorrectionScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - invoice: InvoiceData.PKVInvoice?, - matrix: BitMatrix? -) { - LazyColumn( - modifier = Modifier - .fillMaxSize().padding(innerPadding), - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues(), - state = listState - ) { - item { - Text( - modifier = Modifier.padding(PaddingDefaults.Medium).fillMaxWidth(), - textAlign = TextAlign.Center, - style = AppTheme.typography.subtitle1, - text = stringResource(R.string.invoice_correction_info) - ) - } - matrix?.let { - item { - DataMatrix( - modifier = Modifier - .padding(PaddingDefaults.Medium) - .fillMaxWidth(), - matrix = matrix - ) - } - } - item { - InvoiceLocalCorrectionSection(invoice) - } - } -} - -@Composable -private fun InvoiceLocalCorrectionSection(invoice: InvoiceData.PKVInvoice?) { - SpacerLarge() - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = AppTheme.typography.subtitle1, - text = stringResource(R.string.invoice_correction) - ) - SpacerSmall() - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = AppTheme.typography.body2l, - text = invoice?.medicationRequest?.medication?.name() ?: "" - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreen.kt deleted file mode 100644 index c1bedcb3..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreen.kt +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ImportExport -import androidx.compose.material.icons.rounded.SaveAlt -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.FileProviderAuthority -import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SecondaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.visualTestTag -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -class InvoiceShareScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val pkvNavigationArguments = remember { - val arguments = requireNotNull(navBackStackEntry.arguments) - PkvNavigationArguments( - taskId = requireNotNull(arguments.getString(PkvRoutes.TaskId)), - profileId = requireNotNull(arguments.getString(PkvRoutes.ProfileId)) - ) - } - val invoiceController = rememberInvoiceController(pkvNavigationArguments.profileId) - val scaffoldState = rememberScaffoldState() - - val scope = rememberCoroutineScope() - val context = LocalContext.current - var innerHeight by remember { mutableStateOf(0) } - val listState = rememberLazyListState() - val fileProvider by rememberInstance() - - LaunchedEffect(listState) { - listState.scrollToItem(listState.layoutInfo.totalItemsCount, 0) - } - - AnimatedElevationScaffold( - modifier = Modifier - .imePadding() - .visualTestTag(TestTag.Profile.InvoicesDetailScreen), - topBarTitle = "", - navigationMode = NavigationBarMode.Close, - scaffoldState = scaffoldState, - bottomBar = { - ShareInformationBottomBar { - scope.launch { - invoiceController.detailState(pkvNavigationArguments.taskId).first()?.let { - invoiceController.shareInvoicePDF(context, it, fileProvider) - navController.popBackStack() - } - } - } - }, - listState = listState, - actions = {}, - onBack = { navController.popBackStack() } - ) { innerPadding -> - InvoiceShareScreenContent( - innerPadding, - listState, - setInnerHeight = { innerHeight = it } - ) - - Box( - modifier = Modifier - .fillMaxSize() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .requiredHeight(350.dp) - .background( - brush = Brush.verticalGradient( - startY = 40f, - endY = 450f, - colors = listOf( - AppTheme.colors.neutral000, - Color.Transparent - ) - ) - ) - ) - } - } - } -} - -@Composable -private fun InvoiceShareScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - setInnerHeight: (Int) -> Unit -) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding( - top = innerPadding.calculateTopPadding(), - bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - .onGloballyPositioned { - setInnerHeight(it.boundsInWindow().size.height.toInt()) - }, - horizontalAlignment = Alignment.CenterHorizontally, - state = listState - ) { - item { - Image( - painterResource(R.drawable.share_sheet), - null - ) - } - item { - InformationHeader() - } - item { - InformationLabel( - Icons.Rounded.ImportExport, - stringResource( - R.string.share_information_app_share_info - ) - ) - } - item { - Text( - stringResource(R.string.share_information_or).uppercase(), - style = AppTheme.typography.subtitle2l - ) - } - item { - InformationLabel( - Icons.Rounded.SaveAlt, - stringResource(R.string.share_information_app_save_info) - ) - } - item { - SpacerLarge() - } - } -} - -@Composable -private fun InformationLabel(icon: ImageVector, info: String) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - icon, - null, - tint = AppTheme.colors.neutral600, - modifier = Modifier.padding(end = PaddingDefaults.Medium) - ) - Text( - modifier = Modifier - .weight(1f) - .padding(vertical = PaddingDefaults.Medium), - text = info, - style = AppTheme.typography.body2 - ) - } -} - -@Composable -private fun InformationHeader() { - Text( - text = stringResource(R.string.share_invoice_information_header), - modifier = Modifier - .padding(top = PaddingDefaults.XXLarge), - style = AppTheme.typography.subtitle1 - ) - SpacerSmall() -} - -@Composable -private fun ShareInformationBottomBar(onClickShare: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - SecondaryButton( - onClick = onClickShare, - modifier = Modifier.padding(end = PaddingDefaults.Medium), - contentPadding = PaddingValues(horizontal = PaddingDefaults.XXLarge, vertical = 13.dp) - ) { - Text(text = stringResource(R.string.invoice_share_okay)) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt deleted file mode 100644 index 698886b2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pkv.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowDown -import androidx.compose.material.icons.outlined.KeyboardArrowRight -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -@Composable -fun InvoiceThreeDotMenu( - taskId: String, - onClickShareInvoice: (String) -> Unit, - onClickRemoveInvoice: (String) -> Unit, - onClickCorrectInvoiceLocally: (String) -> Unit -) { - var isMenuExpanded by remember { mutableStateOf(false) } - var isDropDownExpanded by remember { mutableStateOf(false) } - - IconButton( - onClick = { isMenuExpanded = true } - ) { - Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) - } - DropdownMenu( - expanded = isMenuExpanded, - onDismissRequest = { isMenuExpanded = false }, - offset = DpOffset(24.dp, 0.dp) - ) { - DropdownMenuItem( - onClick = { - onClickShareInvoice(taskId) - isMenuExpanded = false - } - ) { - Text( - text = stringResource(R.string.invoice_header_share) - ) - } - if (isDropDownExpanded) { - SpacerSmall() - } - DropdownMenuItem( - onClick = { - isDropDownExpanded = !isDropDownExpanded - } - ) { - Column { - val arrow = when (isDropDownExpanded) { - true -> Icons.Outlined.KeyboardArrowDown - false -> Icons.Outlined.KeyboardArrowRight - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = stringResource(R.string.invoice_menu_correct_invoice)) - SpacerSmall() - Icon(arrow, null, tint = AppTheme.colors.neutral400) - } - - if (isDropDownExpanded) { - DropdownMenuItem(onClick = { - onClickCorrectInvoiceLocally(taskId) - isMenuExpanded = false - }) { - Text( - text = stringResource(R.string.invoice_menu_correct_invoice_locally), - style = AppTheme.typography.subtitle1 - ) - } - DropdownMenuItem(onClick = {}) { - Text( - text = stringResource(R.string.invoice_menu_correct_invoice_online), - style = AppTheme.typography.subtitle1l - ) - } - } - } - } - DropdownMenuItem( - onClick = { - onClickRemoveInvoice(taskId) - isMenuExpanded = false - } - ) { - Text( - text = stringResource(R.string.invoice_header_delete), - color = AppTheme.colors.red600 - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceDialogues.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceDialogues.kt new file mode 100644 index 00000000..5444a804 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceDialogues.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UnusedPrivateMember") + +package de.gematik.ti.erp.app.pkv.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun UserNotLoggedInDialog( + dialogScaffold: DialogScaffold, + onEvent: ComposableEvent, + onConfirmRequest: () -> Unit +) { + onEvent.listen { + dialogScaffold.show { dialog -> + UserNotLoggedInDialog( + onDismissRequest = { + dialog.dismiss() + }, + onConfirmRequest = { + onConfirmRequest() + dialog.dismiss() + } + ) + } + } +} + +@Composable +private fun UserNotLoggedInDialog( + onDismissRequest: () -> Unit, + onConfirmRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.consent_error_connect), + bodyText = stringResource(R.string.invoices_delete_not_logged_in_description), + confirmText = stringResource(R.string.invoices_connect_btn), + dismissText = stringResource(R.string.profile_invoices_cancel), + onDismissRequest = { + onDismissRequest() + }, + onConfirmRequest = { + onConfirmRequest() + } + ) +} + +@Composable +fun RevokeConsentDialog( + dialogScaffold: DialogScaffold, + onRevokeConsentEvent: ComposableEvent, + onRevokeConsent: () -> Unit +) { + onRevokeConsentEvent.listen { + dialogScaffold.show { dialog -> + RevokeConsentDialog( + onDismissRequest = { + dialog.dismiss() + }, + onConfirmRequest = { + onRevokeConsent() + dialog.dismiss() + } + ) + } + } +} + +@Composable +private fun RevokeConsentDialog( + onDismissRequest: () -> Unit, + onConfirmRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.profile_revoke_consent_header), + bodyText = stringResource(R.string.profile_revoke_consent_info), + confirmText = stringResource(R.string.profile_revoke_consent), + dismissText = stringResource(R.string.profile_invoices_cancel), + onDismissRequest = { + onDismissRequest() + }, + onConfirmRequest = { + onConfirmRequest() + } + ) +} + +@Composable +fun DeleteInvoiceDialog( + dialogScaffold: DialogScaffold, + onEvent: ComposableEvent, + onDeleteInvoice: () -> Unit +) { + onEvent.listen { + dialogScaffold.show { + DeleteInvoiceDialog( + onCancel = { + it.dismiss() + }, + onDeleteInvoice = { + onDeleteInvoice() + it.dismiss() + } + ) + } + } +} + +@Composable +private fun DeleteInvoiceDialog( + onCancel: () -> Unit, + onDeleteInvoice: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.profile_delete_invoice_header), + bodyText = stringResource(R.string.profile_delete_invoice_info), + confirmText = stringResource(R.string.profile_delete_invoice), + dismissText = stringResource(R.string.profile_invoices_cancel), + confirmTextColor = AppTheme.colors.red600, + dismissTextColor = AppTheme.colors.primary600, + onDismissRequest = onCancel, + onConfirmRequest = onDeleteInvoice + ) +} + +@Composable +fun GrantConsentDialog( + dialogScaffold: DialogScaffold, + onGrantConsentEvent: ComposableEvent, + onShow: () -> Unit, + onGrantConsent: () -> Unit +) { + onGrantConsentEvent.listen { + dialogScaffold.show { dialog -> + onShow() + GrantConsentDialog( + onDismissRequest = { dialog.dismiss() }, + onConfirmRequest = { + onGrantConsent() + dialog.dismiss() + } + ) + } + } +} + +@Composable +private fun GrantConsentDialog( + onDismissRequest: () -> Unit, + onConfirmRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.get_consent_info_dialog_header), + bodyText = stringResource(R.string.get_consent_info_dialog_info), + confirmText = stringResource(R.string.get_consent_info_dialog_accept), + dismissText = stringResource(R.string.get_consent_info_dialog_cancel), + onDismissRequest = { + onDismissRequest() + }, + onConfirmRequest = { + onConfirmRequest() + } + ) +} + +@LightDarkPreview +@Composable +private fun UserNotLoggedInDialogPreview() { + PreviewAppTheme { + UserNotLoggedInDialog({}, {}) + } +} + +@LightDarkPreview +@Composable +private fun RevokeConsentDialogPreview() { + PreviewAppTheme { + RevokeConsentDialog({}, {}) + } +} + +@LightDarkPreview +@Composable +private fun DeleteInvoiceDialogPreview() { + PreviewAppTheme { + DeleteInvoiceDialog({}, {}) + } +} + +@LightDarkPreview +@Composable +private fun GrantConsentDialogPreview() { + PreviewAppTheme { + GrantConsentDialog({}, {}) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceListLoading.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceListLoading.kt new file mode 100644 index 00000000..973acf4e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceListLoading.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.valentinilk.shimmer.shimmer +import de.gematik.ti.erp.app.shimmer.LimitedTextShimmer +import de.gematik.ti.erp.app.shimmer.RectangularShapeShimmer +import de.gematik.ti.erp.app.shimmer.TinyTextShimmer +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLargeMedium +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("MagicNumber") +@Composable +private fun InvoiceListLoadingItem() { + Column( + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + LimitedTextShimmer(width = SizeDefaults.twentythreefold) + LimitedTextShimmer() + Row { + Column( + modifier = Modifier.weight(0.55f) + ) { + TinyTextShimmer() + SpacerSmall() + RectangularShapeShimmer() + } + Spacer(Modifier.weight(0.3f)) + Icon( + modifier = Modifier.weight(0.15f), + imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.neutral400 + ) + } + + SpacerLarge() + } +} + +@Suppress("MagicNumber") +@Composable +fun InvoiceListLoading() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium) + .shimmer() + ) { + SpacerXXLargeMedium() + repeat(10) { + InvoiceListLoadingItem() + } + } +} + +@LightDarkPreview +@Composable +fun InvoiceListLoadingPreview() { + PreviewAppTheme { + InvoiceListLoading() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceThreeDotMenu.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceThreeDotMenu.kt new file mode 100644 index 00000000..c188ffbb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoiceThreeDotMenu.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall + +@Composable +fun InvoiceThreeDotMenu( + taskId: String, + onClickShareInvoice: (String) -> Unit, + onClickRemoveInvoice: (String) -> Unit, + onClickCorrectInvoiceLocally: (String) -> Unit, + onClickCorrectInvoiceInApp: (String) -> Unit +) { + var isMenuExpanded by remember { mutableStateOf(false) } + var isDropDownExpanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { isMenuExpanded = true } + ) { + Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) + } + DropdownMenu( + expanded = isMenuExpanded, + onDismissRequest = { isMenuExpanded = false }, + offset = DpOffset(SizeDefaults.triple, SizeDefaults.zero) + ) { + DropdownMenuItem( + onClick = { + onClickShareInvoice(taskId) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.invoice_header_share) + ) + } + if (isDropDownExpanded) { + SpacerSmall() + } + DropdownMenuItem( + onClick = { + isDropDownExpanded = !isDropDownExpanded + } + ) { + Column { + val arrow = when (isDropDownExpanded) { + true -> Icons.Outlined.KeyboardArrowDown + false -> Icons.AutoMirrored.Outlined.KeyboardArrowRight + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = stringResource(R.string.invoice_menu_correct_invoice)) + SpacerSmall() + Icon(arrow, null, tint = AppTheme.colors.neutral400) + } + + if (isDropDownExpanded) { + DropdownMenuItem(onClick = { + onClickCorrectInvoiceLocally(taskId) + isMenuExpanded = false + }) { + Text( + text = stringResource(R.string.invoice_menu_correct_invoice_locally), + style = AppTheme.typography.subtitle1 + ) + } + DropdownMenuItem(onClick = { + onClickCorrectInvoiceInApp(taskId) + }) { + Text( + text = stringResource(R.string.invoice_menu_correct_invoice_online), + style = AppTheme.typography.subtitle1l + ) + } + } + } + } + DropdownMenuItem( + onClick = { + onClickRemoveInvoice(taskId) + isMenuExpanded = false + } + ) { + Text( + text = stringResource(R.string.invoice_header_delete), + color = AppTheme.colors.red600 + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoicesEmptyScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoicesEmptyScreen.kt new file mode 100644 index 00000000..096977a6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/components/InvoicesEmptyScreen.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun LazyItemScope.InvoicesEmptyScreen() { + Column( + modifier = + Modifier + .fillParentMaxSize() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painterResource(R.drawable.girl_red_oh_no), + contentDescription = null + ) + Text( + stringResource(R.string.invoices_no_invoices), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.offset(y = -(PaddingDefaults.Large)) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoiceLocalCorrectionScreenPreviewData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoiceLocalCorrectionScreenPreviewData.kt new file mode 100644 index 00000000..9cf073e4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoiceLocalCorrectionScreenPreviewData.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceLocalCorrectionScreenPreviewData.pkvInvoiceRecord +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.asFhirTemporal +import kotlinx.datetime.Instant + +class InvoiceLocalCorrectionScreenPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + pkvInvoiceRecord, + null + ) +} + +object InvoiceLocalCorrectionScreenPreviewData { + + val time: Instant = Instant.parse("2023-06-14T10:15:30Z") + + val pkvInvoiceRecord = InvoiceData.PKVInvoiceRecord( + profileId = "1234", + taskId = "01234", + accessCode = "98765", + timestamp = time, + invoice = InvoiceData.Invoice( + 2.30, + 6.80, + "EUR", + listOf(), + listOf() + ), + pharmacyOrganization = SyncedTaskData.Organization( + "Pharmacy", + SyncedTaskData.Address("", "", "", ""), + null, + null, + null + ), + practitionerOrganization = SyncedTaskData.Organization( + "Practitioner", + SyncedTaskData.Address("", "", "", ""), + null, + null, + null + ), + practitioner = SyncedTaskData.Practitioner("Practitioner", "", ""), + patient = SyncedTaskData.Patient( + "Patient", + SyncedTaskData.Address("", "", "", ""), + null, + null + ), + medicationRequest = SyncedTaskData.MedicationRequest( + SyncedTaskData.Medication( + category = SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, + vaccine = true, + text = "Medication Name", + form = "Form", + lotNumber = "lot number", + expirationDate = null, + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "norm size code", + amount = Ratio( + numerator = Quantity( + value = "2", + unit = "1" + ), + denominator = null + ), + ingredientMedications = emptyList(), + ingredients = emptyList(), + manufacturingInstructions = null, + packaging = null + ), + null, null, SyncedTaskData.AccidentType.None, + null, null, false, null, + SyncedTaskData.MultiplePrescriptionInfo(false), 1, "Note", true, SyncedTaskData.AdditionalFee.NotExempt + ), + whenHandedOver = time.asFhirTemporal(), + consumed = false + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoicePreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoicePreviewParameterProvider.kt new file mode 100644 index 00000000..7b3641a0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/preview/InvoicePreviewParameterProvider.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.pkv.model.InvoiceState +import de.gematik.ti.erp.app.pkv.ui.preview.PkvMockData.invoiceRecord +import de.gematik.ti.erp.app.pkv.ui.preview.PkvMockData.medicationPzn +import de.gematik.ti.erp.app.pkv.ui.preview.PkvMockData.medicationRequest +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Instant +import de.gematik.ti.erp.app.utils.FhirTemporal.Instant as FhirInstant + +data class InvoiceDetailScreenPreviewData( + val isFromPrescriptionDetails: Boolean, + val invoiceState: InvoiceState +) + +data class InvoiceListScreenPreviewData( + val invoices: Map>, + val isSsoTokenValid: Boolean, + val isConsentGranted: Boolean +) + +class InvoiceListScreenPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InvoiceListScreenPreviewData( + invoices = mapOf( + Year(2023) to listOf( + invoiceRecord.copy( + timestamp = Instant.parse("2023-10-23T12:34:56Z"), + medicationRequest = medicationRequest.copy( + medication = medicationPzn.copy( + text = "Medikament 1" + ) + ) + ) + ), + Year(2022) to listOf( + invoiceRecord.copy( + timestamp = Instant.parse("2024-11-23T12:34:56Z"), + medicationRequest = medicationRequest.copy( + medication = medicationPzn.copy( + text = "Medikament 2" + ) + ) + ), + invoiceRecord.copy( + timestamp = Instant.parse("2024-10-23T12:34:56Z"), + medicationRequest = medicationRequest.copy( + medication = medicationPzn.copy( + text = "Medikament 3" + ) + ) + ) + ) + ), + isSsoTokenValid = true, + isConsentGranted = true + ), + InvoiceListScreenPreviewData( + invoices = emptyMap(), + isSsoTokenValid = false, + isConsentGranted = false + ) + ) +} + +class InvoiceDetailScreenPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + InvoiceDetailScreenPreviewData( + isFromPrescriptionDetails = true, + invoiceState = InvoiceState.NoInvoice + ), + InvoiceDetailScreenPreviewData( + isFromPrescriptionDetails = false, + invoiceState = InvoiceState.NoInvoice + ), + InvoiceDetailScreenPreviewData( + isFromPrescriptionDetails = true, + invoiceState = InvoiceState.InvoiceLoaded( + record = invoiceRecord + ) + ) + ) +} + +private object PkvMockData { + val chargeItem = InvoiceData.ChargeableItem( + description = InvoiceData.ChargeableItem.Description.PZN("pzn"), + text = "text", + factor = 2.0, + price = InvoiceData.PriceComponent( + value = 1.0, + tax = 1.0 + ) + ) + val invoice = InvoiceData.Invoice( + totalAdditionalFee = 1.0, + totalBruttoAmount = 489.73, + currency = "currency", + additionalInformation = listOf("additionalInformation"), + chargeableItems = listOf(chargeItem), + additionalDispenseItems = listOf(chargeItem) + ) + val timestamp = Instant.parse("1988-10-23T12:34:56Z") + val handoverTimestamp = Instant.parse(("2021-11-25T15:20:00Z")) + val address = SyncedTaskData.Address( + line1 = "line1", + line2 = "line2", + postalCode = "postalCode", + city = "city" + ) + val medicationPzn = SyncedTaskData.Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Präparat", + form = "AEO", + lotNumber = "lotNumber", + expirationDate = FhirTemporal.Instant(timestamp), + identifier = SyncedTaskData.Identifier("FJHE98383JGK"), + normSizeCode = "FRE4347", + amount = Ratio( + numerator = Quantity( + value = "2", + unit = "oz" + ), + denominator = null + ), + ingredientMedications = emptyList(), + ingredients = emptyList(), + manufacturingInstructions = null, + packaging = null + ) + val medicationRequest = MedicationRequest( + medication = medicationPzn, + dateOfAccident = null, + location = "location", + emergencyFee = true, + dosageInstruction = "dosageInstruction", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "note", + substitutionAllowed = true + ) + val invoiceRecord = InvoiceData.PKVInvoiceRecord( + profileId = "profileId", + taskId = "taskId", + accessCode = "accessCode", + timestamp = timestamp, + pharmacyOrganization = SyncedTaskData.Organization( + name = "Medikamenten Apotheke", + address = address, + uniqueIdentifier = "uniqueIdentifier" + ), + practitionerOrganization = SyncedTaskData.Organization( + name = "practitionerOrganization", + address = address, + uniqueIdentifier = "uniqueIdentifier" + ), + practitioner = SyncedTaskData.Practitioner( + name = "Max Mustermann", + qualification = "qualification", + practitionerIdentifier = "practitionerIdentifier" + ), + patient = SyncedTaskData.Patient( + name = "name", + address = address, + insuranceIdentifier = "insuranceIdentifier", + birthdate = FhirInstant(value = timestamp) + ), + medicationRequest = medicationRequest, + whenHandedOver = FhirTemporal.Instant(value = handoverTimestamp), + invoice = invoice, + consumed = false + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/ConsentMessages.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/ConsentMessages.kt new file mode 100644 index 00000000..54126af6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/ConsentMessages.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +// TODO: Needs to follow SOLID principles +@Suppress("LongParameterList", "CyclomaticComplexMethod") +@Composable +fun HandleConsentState( + consentState: ConsentState, + onDeleteLocalInvoices: () -> Unit = {}, + dialog: DialogScaffold, + onRetry: (ConsentContext) -> Unit, + onShowCardWall: () -> Unit, + onConsentGranted: () -> Unit = {}, + onConsentRevoked: () -> Unit = {}, + onConsentNotGranted: () -> Unit = {} +) { + val alertData = consentErrorData(consentState) + + when (consentState) { + is ConsentState.ConsentErrorState.NoInternet -> + ConsentErrorDialog(dialog, alertData) { + onRetry(consentState.context) + } + ConsentState.ConsentErrorState.BadRequest -> ConsentErrorDialog(dialog, alertData) {} + is ConsentState.ConsentErrorState.ServerTimeout -> + ConsentErrorDialog(dialog, alertData) { + onRetry(consentState.context) + } + is ConsentState.ConsentErrorState.InternalError -> + ConsentErrorDialog(dialog, alertData) { + onRetry(consentState.context) + } + is ConsentState.ConsentErrorState.TooManyRequests -> + ConsentErrorDialog(dialog, alertData) { + onRetry(consentState.context) + } + ConsentState.ConsentErrorState.Forbidden -> ConsentErrorDialog(dialog, alertData) {} + ConsentState.ConsentErrorState.Unauthorized -> + ConsentErrorDialog( + dialog, + alertData, + onShowCardWall + ) + ConsentState.ConsentErrorState.AlreadyGranted -> { /* do nothing */ } + ConsentState.ConsentErrorState.ChargeConsentAlreadyRevoked -> { /* do nothing */ } + ConsentState.ConsentErrorState.Unknown -> { /* do nothing */ } + is ConsentState.ValidState.Granted -> { + if (consentState.context == ConsentContext.GrantConsent) { + onConsentGranted() + } + } + ConsentState.ValidState.Revoked -> { + onDeleteLocalInvoices() + onConsentRevoked() + } + ConsentState.ValidState.NotGranted -> { onConsentNotGranted() } + ConsentState.ValidState.Loading -> { /* do nothing */ } + ConsentState.ValidState.UnknownConsent -> { /* do nothing */ } + } +} + +data class ConsentAlertData( + val header: String, + val info: String, + val cancelText: String, + val actionText: String? = null +) + +@Composable +fun consentErrorData(errorState: ErpServiceState): ConsentAlertData { + val cancelText = stringResource(R.string.consent_error_dialog_cancel) + val retryText = stringResource(R.string.consent_error_dialog_retry) + return when (errorState) { + ConsentState.ConsentErrorState.AlreadyGranted -> + ConsentAlertData( + header = stringResource(R.string.consent_error_already_granted_header), + info = stringResource(R.string.consent_error_already_granted_info), + cancelText = cancelText, + actionText = stringResource(R.string.consent_action_to_invoices) + ) + is ConsentState.ConsentErrorState.NoInternet -> + ConsentAlertData( + header = stringResource(R.string.consent_error_no_internet_header), + info = stringResource(R.string.consent_error_no_internet_info), + cancelText = cancelText, + actionText = retryText + ) + ConsentState.ConsentErrorState.BadRequest -> + ConsentAlertData( + header = stringResource(R.string.consent_error_bad_request_header), + info = stringResource(R.string.consent_error_bad_request_info), + cancelText = cancelText + ) + is ConsentState.ConsentErrorState.ServerTimeout -> + ConsentAlertData( + header = stringResource(R.string.consent_error_server_timeout_header), + info = stringResource(R.string.consent_error_server_timeout_info), + cancelText = cancelText, + actionText = retryText + ) + is ConsentState.ConsentErrorState.InternalError -> + ConsentAlertData( + header = stringResource(R.string.consent_error_internal_error_header), + info = stringResource(R.string.consent_error_internal_error_info), + cancelText = cancelText, + actionText = retryText + ) + is ConsentState.ConsentErrorState.TooManyRequests -> + ConsentAlertData( + header = stringResource(R.string.consent_error_too_many_requests_header), + info = stringResource(R.string.consent_error_too_many_requests_info), + cancelText = cancelText, + actionText = retryText + ) + ConsentState.ConsentErrorState.Forbidden -> + ConsentAlertData( + header = stringResource(R.string.consent_error_forbidden_header), + info = stringResource(R.string.consent_error_forbidden_info), + cancelText = cancelText + ) + ConsentState.ConsentErrorState.Unauthorized -> + ConsentAlertData( + header = stringResource(R.string.consent_error_unauthorized_header), + info = stringResource(R.string.consent_error_unauthorized_info), + cancelText = cancelText, + actionText = stringResource(R.string.consent_error_connect) + ) + else -> { + ConsentAlertData( + header = "", + info = "", + cancelText = cancelText + ) + } + } +} + +@Composable +fun ConsentErrorDialog( + dialogScaffold: DialogScaffold, + alertData: ConsentAlertData, + onAction: () -> Unit +) = if (alertData.actionText != null) { + dialogScaffold.show { dialog -> + ErezeptAlertDialog( + title = alertData.header, + bodyText = alertData.info, + confirmText = alertData.actionText, + dismissText = alertData.cancelText, + onDismissRequest = { + dialog.dismiss() + }, + onConfirmRequest = { + onAction() + dialog.dismiss() + } + ) + } +} else { + dialogScaffold.show { dialog -> + ErezeptAlertDialog( + title = alertData.header, + body = alertData.info, + okText = alertData.cancelText, + onDismissRequest = dialog::dismiss + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceDetailsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceDetailsScreen.kt new file mode 100644 index 00000000..8c8603fb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceDetailsScreen.kt @@ -0,0 +1,453 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.ScaffoldState +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate.joinMedicationInfo +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pkv.model.InvoiceAction +import de.gematik.ti.erp.app.pkv.model.InvoiceState +import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments.Companion.getPkvNavigationArguments +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.pkv.ui.components.DeleteInvoiceDialog +import de.gematik.ti.erp.app.pkv.ui.components.InvoiceThreeDotMenu +import de.gematik.ti.erp.app.pkv.ui.components.InvoicesEmptyScreen +import de.gematik.ti.erp.app.pkv.ui.components.UserNotLoggedInDialog +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceDetailScreenPreviewData +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceDetailScreenPreviewParameterProvider +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LabeledText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.rememberContentPadding +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import de.gematik.ti.erp.app.utils.extensions.show + +class InvoiceDetailsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Suppress("CyclomaticComplexMethod") + @Composable + override fun Content() { + val arguments = remember(navBackStackEntry.arguments) { navBackStackEntry.arguments?.getPkvNavigationArguments() } + + if (arguments == null) { + ErrorScreenComponent(onClickRetry = { navController.popBackStack() }) + return + } + + val dialog = LocalDialog.current + val snackbar = LocalSnackbarScaffold.current + + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + val invoicesDeletedString = stringResource(R.string.invoices_deleted) + + val deleteInvoiceEvent = ComposableEvent() + + val isFromPrescriptionDetails = remember(navController.previousBackStackEntry?.destination?.route) { + navController.previousBackStackEntry?.destination?.route?.contains(PrescriptionDetailRoutes.PrescriptionDetailScreen.route) == true + } + val profileId = arguments.profileId + val controller = rememberInvoiceController(profileId) + val combinedProfileInformation by controller.combinedProfile.collectAsStateWithLifecycle() + + val invoiceState by produceState(InvoiceState.NoInvoice) { + arguments.taskId?.let { it -> + controller.getInvoiceForTaskId(it).collect { + value = if (it != null) { + InvoiceState.InvoiceLoaded(it) + } else { + InvoiceState.NoInvoice + } + } + } + } + + controller.invoiceDetailScreenEvents.deleteSuccessfulEvent.listen { + snackbar.show(invoicesDeletedString, scope) + navController.navigateUp() + } + + UserNotLoggedInDialog( + onEvent = controller.invoiceDetailScreenEvents.askUserToLoginEvent, + dialogScaffold = dialog, + onConfirmRequest = navController::navigateUp + ) + + DeleteInvoiceDialog( + onEvent = deleteInvoiceEvent, + dialogScaffold = dialog, + onDeleteInvoice = { + arguments.taskId?.let { + controller.deleteInvoice(taskId = it) + } + } + ) + + UiStateMachine( + state = combinedProfileInformation, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onError = { + ErrorScreenComponent() + } + ) { (selectedProfile, _) -> + InvoiceDetailScreenScaffold( + listState = listState, + scaffoldState = scaffoldState, + invoiceState = invoiceState, + isFromPrescriptionDetails = isFromPrescriptionDetails, + onAction = { invoiceAction -> + when (invoiceAction) { + is InvoiceAction.Correct -> { + selectedProfile?.let { + navController.navigate( + PkvRoutes.InvoiceLocalCorrectionScreen.path( + taskId = invoiceAction.taskId, + profileId = selectedProfile.id + ) + ) + } + } + + is InvoiceAction.Delete -> deleteInvoiceEvent.trigger() + + is InvoiceAction.Share -> { + selectedProfile?.let { + navController.navigate( + PkvRoutes.InvoiceShareScreen.path( + taskId = invoiceAction.taskId, + profileId = selectedProfile.id + ) + ) + } + } + + is InvoiceAction.Submit -> { + navController.navigate( + PkvRoutes.InvoiceShareScreen.path( + taskId = invoiceAction.taskId, + profileId = profileId + ) + ) + } + + is InvoiceAction.ViewDetail -> { + selectedProfile?.let { + navController.navigate( + PkvRoutes.InvoiceExpandedDetailsScreen.path( + taskId = invoiceAction.taskId, + profileId = selectedProfile.id + ) + ) + } + } + + InvoiceAction.ViewList -> { + if (isFromPrescriptionDetails) { + selectedProfile?.let { + navController.navigate(PkvRoutes.InvoiceListScreen.path(profileId = selectedProfile.id)) + } + } + } + + is InvoiceAction.InAppCorrect -> { + // TODO: to be implemented + if (BuildConfigExtension.isNonReleaseMode) { + snackbar.show( + message = "InAppCorrect is not implemented yet", + scope = scope + ) + } + } + + InvoiceAction.Back -> navController.navigateUp() + } + } + ) + } + } +} + +@Composable +private fun InvoiceDetailScreenScaffold( + listState: LazyListState, + scaffoldState: ScaffoldState, + invoiceState: InvoiceState, + isFromPrescriptionDetails: Boolean, + onAction: (InvoiceAction) -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.imePadding(), + topBarTitle = stringResource(R.string.invoices_detail_title), + navigationMode = NavigationBarMode.Back, + listState = listState, + scaffoldState = scaffoldState, + actions = { + invoiceState.OnInvoiceLoaded { invoices -> + Row { + InvoiceThreeDotMenu( + taskId = invoices.record.taskId, + onClickShareInvoice = { taskId -> onAction(InvoiceAction.Share(taskId, invoices.record.profileId)) }, + onClickRemoveInvoice = { taskId -> onAction(InvoiceAction.Delete(taskId)) }, + onClickCorrectInvoiceLocally = { taskId -> onAction(InvoiceAction.Correct(taskId, invoices.record.profileId)) }, + onClickCorrectInvoiceInApp = { taskId -> onAction(InvoiceAction.InAppCorrect(taskId, invoices.record.profileId)) } + ) + } + } + }, + bottomBar = { + invoiceState.OnInvoiceLoaded { invoices -> + InvoiceDetailBottomBar( + invoices.record.invoice.totalBruttoAmount, + onClickSubmit = { + onAction( + InvoiceAction.Submit( + invoices.record.taskId, + invoices.record.profileId + ) + ) + } + ) + } + }, + onBack = { onAction(InvoiceAction.Back) } + ) { innerPadding -> + InvoiceDetailsScreenContent( + innerPadding = innerPadding, + listState = listState, + invoiceState = invoiceState, + isFromPrescriptionDetails = isFromPrescriptionDetails, + onClickInvoiceList = { onAction(InvoiceAction.ViewList) }, + onClickInvoiceDetail = { taskId -> onAction(InvoiceAction.ViewDetail(taskId)) } + ) + } +} + +@Composable +private fun InvoiceDetailsScreenContent( + innerPadding: PaddingValues, + listState: LazyListState, + invoiceState: InvoiceState, + isFromPrescriptionDetails: Boolean, + onClickInvoiceDetail: (String) -> Unit, + onClickInvoiceList: () -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + val padding by rememberContentPadding(innerPadding) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + if (invoiceState.hasInvoice()) { + val invoice = (invoiceState as InvoiceState.InvoiceLoaded) + item { + InvoiceMedicationHeader(invoice.record) + } + item { + LabeledText( + description = stringResource(R.string.invoice_prescribed_by), + content = invoice.record.practitioner.name + ) + } + item { + LabeledText( + description = stringResource(R.string.invoice_redeemed_in), + content = invoice.record.pharmacyOrganization.name + ) + } + item { + LabeledText( + description = stringResource(R.string.invoice_redeemed_on), + content = invoice.record.whenHandedOver?.formattedString() + ) + } + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + TertiaryButton( + onClick = { + onClickInvoiceDetail(invoice.record.taskId) + } + ) { + Text(text = stringResource(R.string.invoice_show_more)) + } + } + } + item { + if (isFromPrescriptionDetails && invoiceState.hasInvoice()) { + // check if navigation comes from PrescriptionDetailScreen + LinkToInvoiceList( + modifier = Modifier.padding(vertical = PaddingDefaults.Medium + innerPadding.calculateBottomPadding()) + ) { + onClickInvoiceList() + } + } + } + } else { + item { + Center { + InvoicesEmptyScreen() + } + } + } + } + } +} + +@Composable +private fun InvoiceDetailBottomBar( + totalGrossAmount: Double, + onClickSubmit: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .background(color = AppTheme.colors.neutral100), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.padding(PaddingDefaults.Medium)) { + Text( + stringResource(R.string.invoice_details_cost, totalGrossAmount), + style = AppTheme.typography.h6, + fontWeight = FontWeight.Bold + ) + Text(stringResource(R.string.invoice_detail_total_brutto_amount), style = AppTheme.typography.body2l) + } + + Button( + onClick = onClickSubmit, + modifier = Modifier.padding(end = PaddingDefaults.Medium) + ) { + Text(text = stringResource(R.string.invoice_details_submit)) + } + } +} + +@Composable +private fun LinkToInvoiceList( + modifier: Modifier, + onClickInvoiceList: () -> Unit +) { + Row( + modifier = Modifier + .clickable { onClickInvoiceList() } + .padding(PaddingDefaults.Medium) + .wrapContentWidth() + .then(modifier), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.link_to_invoice_list), + style = AppTheme.typography.body2, + color = AppTheme.colors.primary600 + ) + SpacerTiny() + Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.primary600) + } +} + +// TODO: move to components +@Composable +fun InvoiceMedicationHeader(invoice: InvoiceData.PKVInvoiceRecord) { + val medicationInfo = joinMedicationInfo(invoice.medicationRequest) + Text(text = medicationInfo, style = AppTheme.typography.h5) +} + +@LightDarkPreview +@Composable +fun InvoiceDetailsScreenPreview( + @PreviewParameter(InvoiceDetailScreenPreviewParameterProvider::class) previewData: InvoiceDetailScreenPreviewData +) { + PreviewAppTheme { + InvoiceDetailScreenScaffold( + listState = rememberLazyListState(), + scaffoldState = rememberScaffoldState(), + invoiceState = previewData.invoiceState, + isFromPrescriptionDetails = previewData.isFromPrescriptionDetails, + onAction = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceExpandedDetailsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceExpandedDetailsScreen.kt new file mode 100644 index 00000000..8a2539a0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceExpandedDetailsScreen.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate.joinMedicationInfo +import de.gematik.ti.erp.app.invoice.model.currencyString +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments.Companion.getPkvNavigationArguments +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LabeledText +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode + +class InvoiceExpandedDetailsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val arguments = remember { navBackStackEntry.arguments?.getPkvNavigationArguments() } ?: return + + arguments.profileId.let { profileId -> + val invoiceController = rememberInvoiceController(profileId) + val listState = rememberLazyListState() + val invoice by produceState(null) { + arguments.taskId?.let { taskId -> + invoiceController.getInvoiceForTaskId(taskId).collect { + value = it + } + } + } + AnimatedElevationScaffold( + modifier = Modifier.imePadding(), + topBarTitle = joinMedicationInfo(invoice?.medicationRequest), + navigationMode = NavigationBarMode.Back, + listState = listState, + actions = {}, + onBack = { navController.popBackStack() } + ) { _ -> + InvoiceExpandedDetailsScreenContent( + listState = listState, + invoice = invoice + ) + } + } + } +} + +@Composable +private fun InvoiceExpandedDetailsScreenContent( + listState: LazyListState, + invoice: InvoiceData.PKVInvoiceRecord? +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = PaddingDefaults.Medium), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + invoice?.let { + item { + InvoiceMedicationHeader(it) + } + item { + LabeledText(description = stringResource(R.string.invoice_task_id), content = it.taskId) + } + item { + PatientLabel(it.patient) + } + item { + PractitionerLabel(it.practitioner, it.practitionerOrganization) + } + item { + PharmacyLabel(it.pharmacyOrganization) + } + item { + LabeledText( + description = stringResource(R.string.invoice_redeemed_on), + content = it.whenHandedOver?.formattedString() + ) + } + item { + PriceData(it.invoice) + } + } + } +} + +@Composable +private fun PriceData(invoice: InvoiceData.Invoice) { + val (fees, articles) = invoice.chargeableItems.partition { + (it.description as? InvoiceData.ChargeableItem.Description.PZN)?.isSpecialPZN() ?: false + } + + articles.map { + val article = when (it.description) { + is InvoiceData.ChargeableItem.Description.HMNR -> + stringResource( + R.string.invoice_description_hmknr, + (it.description as InvoiceData.ChargeableItem.Description.HMNR).hmnr + ) + + is InvoiceData.ChargeableItem.Description.PZN -> + stringResource( + R.string.invoice_description_pzn, + (it.description as InvoiceData.ChargeableItem.Description.PZN).pzn + ) + + is InvoiceData.ChargeableItem.Description.TA1 -> + stringResource( + R.string.invoice_description_ta1, + (it.description as InvoiceData.ChargeableItem.Description.TA1).ta1 + ) + } + + Text(stringResource(R.string.invoice_description_articel, article)) + Text(stringResource(R.string.invoice_description_factor, it.factor)) + Text(stringResource(R.string.invoice_description_tax, it.price.tax.currencyString())) + Text(stringResource(R.string.invoice_description_brutto_price, it.price.value)) + + SpacerMedium() + } + + if (fees.isNotEmpty()) { + Text(stringResource(R.string.invoice_description_additional_fees)) + fees.map { + require(it.description is InvoiceData.ChargeableItem.Description.PZN) + val article = when ( + InvoiceData.SpecialPZN.valueOfPZN( + (it.description as InvoiceData.ChargeableItem.Description.PZN).pzn + ) + ) { + InvoiceData.SpecialPZN.EmergencyServiceFee -> stringResource(R.string.invoice_details_emergency_fee) + InvoiceData.SpecialPZN.BTMFee -> stringResource(R.string.invoice_details_narcotic_fee) + InvoiceData.SpecialPZN.TPrescriptionFee -> stringResource(R.string.invoice_details_t_prescription_fee) + InvoiceData.SpecialPZN.ProvisioningCosts -> stringResource(R.string.invoice_details_provisioning_costs) + InvoiceData.SpecialPZN.DeliveryServiceCosts -> + stringResource(R.string.invoice_details_delivery_service_costs) + + null -> error("wrong mapping") + } + + Text(stringResource(R.string.invoice_description_articel, article)) + Text(stringResource(R.string.invoice_description_brutto_price, it.price.value)) + + SpacerMedium() + } + } + + Text(stringResource(R.string.invoice_description_total_brutto_amount, invoice.totalBruttoAmount)) + + Text(stringResource(R.string.invoice_detail_dispense), style = AppTheme.typography.body2l) + SpacerXXLarge() +} + +@Composable +private fun PharmacyLabel(pharmacyOrganization: SyncedTaskData.Organization) { + LabeledTextItems( + label = stringResource(R.string.invoice_redeemed_in), + items = listOf( + pharmacyOrganization.name, + pharmacyOrganization.address?.joinToString(), + pharmacyOrganization.uniqueIdentifier?.let { stringResource(R.string.invoice_pharmacy_id, it) } + ) + ) +} + +@Composable +private fun PractitionerLabel( + practitioner: SyncedTaskData.Practitioner, + practitionerOrganization: SyncedTaskData.Organization +) { + LabeledTextItems( + label = stringResource(R.string.invoice_prescribed_by), + items = listOf( + practitioner.name, + practitionerOrganization.address?.joinToString(), + practitioner.practitionerIdentifier?.let { stringResource(R.string.invoice_practitioner_id, it) } + ) + ) +} + +@Composable +private fun LabeledTextItems(label: String, items: List) { + val showLabel = items.any { it != null } + items.forEach { + it?.let { + Text(it, style = AppTheme.typography.body1) + } + } + if (showLabel) { + Text(label, style = AppTheme.typography.body2l) + } +} + +@Composable +private fun PatientLabel(patient: SyncedTaskData.Patient) { + LabeledTextItems( + label = stringResource(R.string.invoice_prescribed_for), + items = listOf( + patient.name, + patient.insuranceIdentifier?.let { stringResource(R.string.invoice_insurance_id, it) }, + patient.address?.joinToString(), + patient.birthdate?.formattedString()?.let { stringResource(R.string.invoice_born_on, it) } + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceListScreen.kt new file mode 100644 index 00000000..76a306b8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceListScreen.kt @@ -0,0 +1,661 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ScaffoldState +import androidx.compose.material.SnackbarHost +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.authentication.ui.components.AuthenticationFailureDialog +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isNotGranted +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.model.currencyString +import de.gematik.ti.erp.app.launchedeffect.LaunchedEffectOnStart +import de.gematik.ti.erp.app.loading.LoadingIndicator +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes.getPkvNavigationProfileId +import de.gematik.ti.erp.app.pkv.presentation.ConsentValidator +import de.gematik.ti.erp.app.pkv.presentation.rememberConsentController +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.pkv.ui.components.GrantConsentDialog +import de.gematik.ti.erp.app.pkv.ui.components.InvoiceListLoading +import de.gematik.ti.erp.app.pkv.ui.components.InvoicesEmptyScreen +import de.gematik.ti.erp.app.pkv.ui.components.RevokeConsentDialog +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceListScreenPreviewData +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceListScreenPreviewParameterProvider +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.pulltorefresh.PullToRefresh +import de.gematik.ti.erp.app.pulltorefresh.extensions.trigger +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ConnectBottomBar +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import de.gematik.ti.erp.app.utils.extensions.show +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.collectLatest +import kotlinx.datetime.TimeZone.Companion.currentSystemDefault +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +class InvoiceListScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @OptIn(ExperimentalMaterial3Api::class) + @Suppress("LongMethod", "CyclomaticComplexMethod") + @Composable + override fun Content() { + val profileId = navBackStackEntry.arguments.getPkvNavigationProfileId() + + val context = LocalContext.current + val snackbar = LocalSnackbarScaffold.current + val dialog = LocalDialog.current + val intentHandler = LocalIntentHandler.current + + val consentRevokedInfo = stringResource(R.string.consent_revoked_info) + val consentGrantedInfo = stringResource(R.string.consent_granted_info) + + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + val pullToRefreshState = rememberPullToRefreshState() + + val invoiceController = rememberInvoiceController(profileId) + val consentController = rememberConsentController() + + val invoicesState: UiState>> by invoiceController.invoices.collectAsStateWithLifecycle() + val isSsoTokenValid by invoiceController.isSsoTokenValidForSelectedProfile.collectAsStateWithLifecycle() + val isRefreshing by invoiceController.isRefreshing.collectAsStateWithLifecycle() + + val onGrantConsentEvent = ComposableEvent() + val onRevokeConsentEvent = ComposableEvent() + + val consentState by consentController.consentState.collectAsStateWithLifecycle() + val isConsentGranted by consentController.isConsentGranted.collectAsStateWithLifecycle() + val isConsentNotGranted by consentController.isConsentNotGranted.collectAsStateWithLifecycle() + + var consentRecentlyRevoked by remember { mutableStateOf(false) } // required for back navigation + + val onBack: () -> Unit = { + // when navigation came from prescription details and + // the user removes the consent we need to skip the invoice detail screen + if (isConsentGranted && !consentRecentlyRevoked) { + navController.popBackStack() + } else { + navController.popBackStack(PkvRoutes.subGraphName(), true) + } + } + + BackHandler { onBack() } + + navBackStackEntry.onReturnAction(PkvRoutes.InvoiceListScreen) { + invoiceController.refreshCombinedProfile() + invoiceController.downloadInvoices() + invoiceController.invoiceListScreenEvents.getConsentEvent.trigger(profileId) + } + + with(invoiceController) { + invoiceListScreenEvents.invoiceErrorEvent.listen { error -> + snackbar.show( + message = invoiceController.invoiceErrorMessage(context, error.errorState), + scope = scope + ) + } + invoiceListScreenEvents.downloadCompletedEvent.listen { + pullToRefreshState.endRefresh() + } + + showCardWallEvent.listen { id -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(id)) + } + showCardWallWithFilledCanEvent.listen { cardWallData -> + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = cardWallData.profileId, + can = cardWallData.can + ) + ) + } + showGidEvent.listen { gidData -> + navController.navigate( + CardWallRoutes.CardWallIntroScreen.pathWithGid( + profileIdentifier = gidData.profileId, + gidEventData = gidData + ) + ) + } + + invoiceListScreenEvents.getConsentEvent.listen { profileId -> + consentController.getChargeConsent(profileId) + } + } + + AuthenticationFailureDialog( + event = invoiceController.invoiceListScreenEvents.showAuthenticationErrorDialog, + dialogScaffold = dialog + ) + + RevokeConsentDialog( + dialogScaffold = dialog, + onRevokeConsentEvent = onRevokeConsentEvent, + onRevokeConsent = { + consentRecentlyRevoked = true + invoiceController.deleteLocalInvoices() + consentController.revokeChargeConsent(profileId) + consentController.saveConsentDrawerShown(profileId) + navController.navigateAndClearStack(route = PrescriptionRoutes.PrescriptionsScreen.route) + } + ) + + GrantConsentDialog( + dialogScaffold = dialog, + onGrantConsentEvent = onGrantConsentEvent, + onShow = { pullToRefreshState.endRefresh() }, + onGrantConsent = { consentController.grantChargeConsent(profileId) } + ) + + LaunchedEffectOnStart { + consentController.getChargeConsent(profileId) + } + + LaunchedEffect(isConsentNotGranted) { + if (isConsentNotGranted) { + onGrantConsentEvent.trigger() + } + } + + HandleConsentState( + consentState = consentState, + dialog = dialog, + onRetry = { consentContext -> + when (consentContext) { + ConsentContext.GetConsent -> invoiceController.invoiceListScreenEvents.getConsentEvent.trigger(profileId) + ConsentContext.GrantConsent -> { + consentController.grantChargeConsent(profileId) + } + + ConsentContext.RevokeConsent -> consentController.revokeChargeConsent(profileId) + } + }, + onShowCardWall = { invoiceController.chooseAuthenticationMethod(profileId) }, + onDeleteLocalInvoices = { invoiceController.deleteLocalInvoices() }, + onConsentGranted = { snackbar.show(consentGrantedInfo, scope) }, + onConsentRevoked = { snackbar.show(consentRevokedInfo, scope) } + ) + + LaunchedEffect(Unit) { + intentHandler.gidSuccessfulIntent.collectLatest { + invoiceController.refreshCombinedProfile() + invoiceController.downloadInvoices() + } + + if (isSsoTokenValid) { + invoiceController.downloadInvoices() + } + } + + with(pullToRefreshState) { + trigger( + onStartRefreshing = { startRefresh() }, + block = { + ConsentValidator.validateAndExecute( + isSsoTokenValid = isSsoTokenValid, + consentState = consentState, + getChargeConsent = { invoiceController.invoiceListScreenEvents.getConsentEvent.trigger(profileId) }, + onConsentGranted = invoiceController::downloadInvoices, + grantConsent = { onGrantConsentEvent.trigger() } + ) + }, + onNavigation = { + if (!isSsoTokenValid) { + endRefresh() + invoiceController.chooseAuthenticationMethod(profileId) + } + } + ) + } + + LaunchedEffect(isSsoTokenValid, consentState.isNotGranted()) { + if (isSsoTokenValid && consentState.isNotGranted()) { + onGrantConsentEvent.trigger() + } + } + + Box( + Modifier + .fillMaxSize() + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + InvoiceListScreenScaffold( + profileId = profileId, + isSsoTokenValid = isSsoTokenValid, + listState = listState, + scaffoldState = scaffoldState, + isConsentGranted = isConsentGranted, + consentState = consentState, + invoicesState = invoicesState, + onClickConnect = { _ -> + invoiceController.chooseAuthenticationMethod(profileId) + }, + onClickInvoice = { id, taskId -> + navController.navigate(PkvRoutes.InvoiceDetailsScreen.path(taskId = taskId, profileId = id)) + }, + onClickGrantConsent = { onGrantConsentEvent.trigger() }, + onClickRevokeConsent = { onRevokeConsentEvent.trigger() }, + onBack = onBack + ) + + if (isRefreshing) { + LoadingIndicator() + } + + PullToRefresh( + modifier = Modifier.align(Alignment.TopCenter), + pullToRefreshState = pullToRefreshState + ) + } + } +} + +@Composable +private fun InvoiceListScreenScaffold( + profileId: ProfileIdentifier, + isSsoTokenValid: Boolean, + listState: LazyListState, + scaffoldState: ScaffoldState, + consentState: ConsentState, + isConsentGranted: Boolean, + invoicesState: UiState>>, + onClickConnect: (profileId: ProfileIdentifier) -> Unit, + onClickInvoice: (profileId: ProfileIdentifier, taskId: String) -> Unit, + onClickGrantConsent: () -> Unit, + onClickRevokeConsent: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.imePadding(), + topBarTitle = stringResource(R.string.profile_invoices), + snackbarHost = { SnackbarHost(it, modifier = Modifier.systemBarsPadding()) }, + bottomBar = { + when { + !isSsoTokenValid -> { + ConnectBottomBar( + infoText = stringResource(R.string.invoices_connect_info) + ) { + onClickConnect(profileId) + } + } + + isSsoTokenValid && !isConsentGranted -> { + // TODO: Need text for this case + } + } + }, + navigationMode = NavigationBarMode.Back, + scaffoldState = scaffoldState, + listState = listState, + actions = { + if (consentState !is ConsentState.ValidState.Loading && isSsoTokenValid) { + Row { + InvoicesHeaderThreeDotMenu( + consentGranted = isConsentGranted, + onClickGrantConsent = { onClickGrantConsent() }, + onClickRevokeConsent = { onClickRevokeConsent() } + ) + } + } + }, + onBack = onBack + ) { + RefreshInvoicesContent( + invoicesState = invoicesState, + listState = listState, + onClickInvoice = { taskId -> onClickInvoice(profileId, taskId) } + ) + } +} + +@Composable +private fun RefreshInvoicesContent( + listState: LazyListState, + invoicesState: UiState>>, + onClickInvoice: (String) -> Unit +) { + UiStateMachine( + state = invoicesState, + onLoading = { InvoiceListLoading() } + ) { invoices -> + Invoices( + listState = listState, + invoices = invoices, + onClickInvoice = onClickInvoice + ) + } +} + +@Composable +private fun InvoicesHeaderThreeDotMenu( + consentGranted: Boolean, + onClickGrantConsent: () -> Unit, + onClickRevokeConsent: () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true } + ) { + Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = DpOffset(SizeDefaults.triple, SizeDefaults.zero) + ) { + DropdownMenuItem( + onClick = { + if (consentGranted) { + onClickRevokeConsent() + } else { + onClickGrantConsent() + } + expanded = false + } + ) { + Text( + text = + if (consentGranted) { + stringResource(R.string.profile_revoke_consent) + } else { + stringResource(R.string.profile_grant_consent) + }, + color = + if (consentGranted) { + AppTheme.colors.red600 + } else { + AppTheme.colors.neutral900 + } + ) + } + } +} + +@Composable +private fun Invoices( + listState: LazyListState, + invoices: Map>, + onClickInvoice: (String) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues(), + state = listState + ) { + if (invoices.entries.isEmpty()) { + item { + InvoicesEmptyScreen() + } + } else { + invoices.entries.forEach { entry -> + item { + val formattedYear = + remember { + val dateFormatter = DateTimeFormatter.ofPattern("yyyy") + if (entry.value.isNotEmpty()) { + entry.value[0] + .timestamp + .toLocalDateTime(currentSystemDefault()) + .toJavaLocalDateTime() + .format(dateFormatter) + } else { + "" + } + } + + val totalSumOfInvoices = + entry.value + .sumOf { + it.invoice.totalBruttoAmount + }.currencyString() + SpacerMedium() + HeadingPerYear(formattedYear, totalSumOfInvoices, entry.value[0].invoice.currency) + } + entry.value.forEach { invoice -> + item { + val formattedDate = + remember(invoice) { + val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + invoice.timestamp + .toLocalDateTime(currentSystemDefault()) + .toJavaLocalDateTime() + .format(dateFormatter) + } + Invoice( + invoice = invoice, + formattedDate = formattedDate, + onClickInvoice = onClickInvoice + ) + Divider(modifier = Modifier.padding(start = PaddingDefaults.Medium)) + } + } + item { SpacerXLarge() } + } + } + } +} + +@Composable +private fun Invoice( + invoice: InvoiceData.PKVInvoiceRecord, + formattedDate: String, + onClickInvoice: (String) -> Unit +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + onClickInvoice(invoice.taskId) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = + Modifier + .padding( + start = PaddingDefaults.Medium, + top = PaddingDefaults.Medium, + bottom = PaddingDefaults.Medium + ) + .weight(1f) + ) { + val itemName = invoice.medicationRequest.medication?.name() ?: "" + + Text( + itemName, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral900 + ) + SpacerTiny() + Text( + formattedDate, + overflow = TextOverflow.Ellipsis, + style = AppTheme.typography.body2l + ) + SpacerSmall() + TotalBruttoAmountChip( + text = invoice.invoice.totalBruttoAmount.currencyString() + " " + invoice.invoice.currency + ) + } + Row(modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), horizontalArrangement = Arrangement.End) { + Icon(Icons.AutoMirrored.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } + } +} + +@Composable +private fun TotalBruttoAmountChip( + text: String, + modifier: Modifier = Modifier +) { + val shape = RoundedCornerShape(8.dp) + + Row( + Modifier + .background(AppTheme.colors.neutral025, shape) + .border(1.dp, AppTheme.colors.neutral300, shape) + .clip(shape) + .then(modifier) + .padding(vertical = 6.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text, style = AppTheme.typography.subtitle2, color = AppTheme.colors.neutral900) + } +} + +@Composable +private fun HeadingPerYear( + formattedYear: String, + totalSumOfInvoices: String, + currency: String +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = formattedYear, + style = AppTheme.typography.h6 + ) + Text( + text = + stringResource( + R.string.pkv_invoices_total_of_year, + totalSumOfInvoices, + currency + ), + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600 + ) + } +} + +@LightDarkPreview +@Composable +fun InvoiceListScreenScaffoldPreview( + @PreviewParameter(InvoiceListScreenPreviewParameterProvider::class) previewData: InvoiceListScreenPreviewData +) { + PreviewAppTheme { + InvoiceListScreenScaffold( + profileId = "profileId", + isSsoTokenValid = previewData.isSsoTokenValid, + listState = rememberLazyListState(), + scaffoldState = rememberScaffoldState(), + isConsentGranted = previewData.isConsentGranted, + consentState = ConsentState.ValidState.Loading, + invoicesState = UiState.Data(previewData.invoices), + onClickConnect = {}, + onClickInvoice = { _, _ -> }, + onClickGrantConsent = {}, + onClickRevokeConsent = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceLocalCorrectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceLocalCorrectionScreen.kt new file mode 100644 index 00000000..85c1eb9c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceLocalCorrectionScreen.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ScaffoldState +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.google.zxing.common.BitMatrix +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceLocalCorrectionScreenPreviewParameterProvider +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.DataMatrix +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.createBitMatrix +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.forceBrightness + +class InvoiceLocalCorrectionScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val pkvNavigationArguments = remember { + val arguments = requireNotNull(navBackStackEntry.arguments) + PkvNavigationArguments( + taskId = requireNotNull(arguments.getString(PkvRoutes.PKV_NAV_TASK_ID)), + profileId = requireNotNull(arguments.getString(PkvRoutes.PKV_NAV_PROFILE_ID)) + ) + } + val invoiceController = pkvNavigationArguments.profileId?.let { rememberInvoiceController(it) } + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + val invoice by produceState(null) { + pkvNavigationArguments.taskId?.let { + invoiceController?.getInvoiceForTaskId(it)?.collect { + value = it + } + } + } + val matrix = remember(invoice) { + invoice?.dmcPayload?.let { + createBitMatrix(it) + } + } + val activity = LocalActivity.current + activity.forceBrightness() + + InvoiceLocalCorrectionScreenScaffold( + listState = listState, + scaffoldState = scaffoldState, + onBack = navController::navigateUp, + onActionClick = navController::navigateUp, + invoice = invoice, + matrix = matrix + ) + } +} + +@Composable +fun InvoiceLocalCorrectionScreenScaffold( + listState: LazyListState, + scaffoldState: ScaffoldState, + onBack: () -> Unit, + onActionClick: () -> Unit, + invoice: InvoiceData.PKVInvoiceRecord?, + matrix: BitMatrix? +) { + AnimatedElevationScaffold( + modifier = Modifier.imePadding(), + topBarTitle = "", + navigationMode = null, + scaffoldState = scaffoldState, + listState = listState, + actions = { + TextButton(onClick = onActionClick) { + Text(stringResource(R.string.invoice_correct_done)) + } + }, + onBack = onBack + ) { + InvoiceLocalCorrectionScreenContent( + listState = listState, + record = invoice, + matrix = matrix + ) + } +} + +@Composable +private fun InvoiceLocalCorrectionScreenContent( + listState: LazyListState, + record: InvoiceData.PKVInvoiceRecord?, + matrix: BitMatrix? +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues(), + state = listState + ) { + item { + Text( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth(), + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle1, + text = stringResource(R.string.invoice_correction_info) + ) + } + if (matrix != null) { + item { + DataMatrix( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth(), + matrix = matrix + ) + } + } + item { + InvoiceLocalCorrectionSection(record) + } + } +} + +@Composable +private fun InvoiceLocalCorrectionSection(invoice: InvoiceData.PKVInvoiceRecord?) { + SpacerLarge() + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle1, + text = stringResource(R.string.invoice_correction) + ) + SpacerSmall() + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = AppTheme.typography.body2l, + text = invoice?.medicationRequest?.medication?.name() ?: "" + ) +} + +@LightDarkPreview +@Composable +fun InvoiceLocalCorrectionContentPreview( + @PreviewParameter(InvoiceLocalCorrectionScreenPreviewParameterProvider::class) invoice: InvoiceData.PKVInvoiceRecord? +) { + PreviewAppTheme { + InvoiceLocalCorrectionScreenScaffold( + listState = rememberLazyListState(), + scaffoldState = rememberScaffoldState(), + onBack = {}, + onActionClick = {}, + invoice = invoice, + matrix = invoice?.let { createBitMatrix(it.dmcPayload) } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceShareScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceShareScreen.kt new file mode 100644 index 00000000..3acd7503 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/screens/InvoiceShareScreen.kt @@ -0,0 +1,286 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Icon +import androidx.compose.material.ScaffoldState +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ImportExport +import androidx.compose.material.icons.rounded.SaveAlt +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pkv.FileProviderAuthority +import de.gematik.ti.erp.app.pkv.navigation.PkvNavigationArguments.Companion.getPkvNavigationArguments +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SecondaryButton +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import org.kodein.di.compose.rememberInstance + +class InvoiceShareScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val arguments = remember { navBackStackEntry.arguments.getPkvNavigationArguments() } + val invoiceController = rememberInvoiceController(arguments.profileId) + val scaffoldState = rememberScaffoldState() + + val context = LocalContext.current + var innerHeight by remember { mutableIntStateOf(0) } + val listState = rememberLazyListState() + val fileProvider by rememberInstance() + + LaunchedEffect(listState) { + listState.scrollToItem(listState.layoutInfo.totalItemsCount, 0) + } + + InvoiceShareScreenScaffold( + scaffoldState = scaffoldState, + listState = listState, + onBottomBarClick = { + arguments.taskId?.let { + invoiceController.shareInvoice( + context = context, + taskId = it, + fileProvider = fileProvider, + onCompletion = navController::popBackStack + ) + } + }, + onBack = navController::popBackStack + ) { innerPadding -> + InvoiceShareScreenContent( + innerPadding, + listState, + setInnerHeight = { innerHeight = it } + ) + Box( + modifier = Modifier + .fillMaxWidth() + .requiredHeight(SizeDefaults.fortyFourfold) + .background( + brush = Brush.verticalGradient( + startY = 40f, + endY = 450f, + colors = listOf( + AppTheme.colors.neutral000, + Color.Transparent + ) + ) + ) + ) + } + } +} + +@Composable +fun InvoiceShareScreenScaffold( + scaffoldState: ScaffoldState, + listState: LazyListState, + onBottomBarClick: () -> Unit, + onBack: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier + .imePadding() + .testTag(TestTag.Profile.InvoiceShareScreen), + topBarTitle = "", + navigationMode = NavigationBarMode.Close, + scaffoldState = scaffoldState, + bottomBar = { + ShareInformationBottomBar(onClickShare = onBottomBarClick) + }, + listState = listState, + actions = {}, + onBack = onBack, + content = content + ) +} + +@Composable +private fun InvoiceShareScreenContent( + innerPadding: PaddingValues, + listState: LazyListState, + setInnerHeight: (Int) -> Unit +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + .onGloballyPositioned { + setInnerHeight(it.boundsInWindow().size.height.toInt()) + }, + horizontalAlignment = Alignment.CenterHorizontally, + state = listState + ) { + item { + Image( + painter = painterResource(R.drawable.share_sheet), + contentDescription = null + ) + } + item { + InformationHeader() + } + item { + InformationLabel( + Icons.Rounded.ImportExport, + stringResource(R.string.share_information_app_share_info) + ) + } + item { + Text( + stringResource(R.string.share_information_or).uppercase(), + style = AppTheme.typography.subtitle2l + ) + } + item { + InformationLabel( + Icons.Rounded.SaveAlt, + stringResource(R.string.share_information_app_save_info) + ) + } + item { + SpacerLarge() + } + } +} + +@Composable +private fun InformationLabel(icon: ImageVector, info: String) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = AppTheme.colors.neutral600, + modifier = Modifier.padding(end = PaddingDefaults.Medium) + ) + Text( + modifier = Modifier + .weight(1f) + .padding(vertical = PaddingDefaults.Medium), + text = info, + style = AppTheme.typography.body2 + ) + } +} + +@Composable +private fun InformationHeader() { + Text( + text = stringResource(R.string.share_invoice_information_header), + modifier = Modifier + .padding(top = PaddingDefaults.XXLarge), + style = AppTheme.typography.subtitle1 + ) + SpacerSmall() +} + +@Composable +private fun ShareInformationBottomBar(onClickShare: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + SecondaryButton( + onClick = onClickShare, + modifier = Modifier.padding(end = PaddingDefaults.Medium), + contentPadding = PaddingValues(horizontal = PaddingDefaults.XXLarge, vertical = PaddingDefaults.ShortMedium) + ) { + Text(text = stringResource(R.string.invoice_share_okay)) + } + } +} + +@LightDarkPreview +@Composable +fun InvoiceShareScreenScaffoldPreview() { + PreviewAppTheme { + InvoiceShareScreenScaffold( + scaffoldState = rememberScaffoldState(), + listState = rememberLazyListState(), + onBottomBarClick = {}, + onBack = {} + ) { + InvoiceShareScreenContent( + innerPadding = PaddingValues(), + listState = rememberLazyListState(), + setInnerHeight = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt index a0b7341c..907a23bb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pkv.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/ShareInvoiceUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/ShareInvoiceUseCase.kt new file mode 100644 index 00000000..fa44333e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/ShareInvoiceUseCase.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.usecase + +import android.content.Context +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.pkv.FileProviderAuthority +import de.gematik.ti.erp.app.utils.asFhirTemporal +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ShareInvoiceUseCase( + private val repository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + invoice: InvoiceData.PKVInvoiceRecord, + context: Context, + fileProviderAuthority: FileProviderAuthority + ) { + withContext(dispatcher) { + val html = PkvHtmlTemplate.createHTML(invoice) + val file = createSharableFileInCache(context, "invoices", "invoice") + writePdfFromHtml(context, "Invoice_${invoice.taskId}", html, file) + repository.loadInvoiceAttachments(invoice.taskId)?.let { attachments -> + writePDFAttachments(file, attachments) + } + val subject = invoice.medicationRequest.medication?.name() + "_" + + invoice.timestamp.asFhirTemporal().formattedString() + sharePDFFile(context, file, subject, fileProviderAuthority) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt index 3d128e54..8e95cc66 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt @@ -1,55 +1,88 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription +import de.gematik.ti.erp.app.base.usecase.DownloadAllResourcesUseCase import de.gematik.ti.erp.app.prescription.repository.DefaultPrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.DownloadResourcesStateRepository import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository import de.gematik.ti.erp.app.prescription.ui.TwoDCodeProcessor import de.gematik.ti.erp.app.prescription.ui.TwoDCodeScanner import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator -import de.gematik.ti.erp.app.prescription.usecase.GeneratePrescriptionDetailsUseCase +import de.gematik.ti.erp.app.prescription.usecase.DeletePrescriptionUseCase import de.gematik.ti.erp.app.prescription.usecase.GetActivePrescriptionsUseCase import de.gematik.ti.erp.app.prescription.usecase.GetArchivedPrescriptionsUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetDownloadResourcesDetailStateUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetDownloadResourcesSnapshotStateUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.RedeemScannedTaskUseCase import de.gematik.ti.erp.app.prescription.usecase.RefreshPrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.SaveWelcomeMessageUseCase import de.gematik.ti.erp.app.prescription.usecase.UpdateScannedTaskNameUseCase import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.bindSingleton import org.kodein.di.instance -val prescriptionModule = DI.Module("prescriptionModule") { - bindProvider { TwoDCodeProcessor() } - bindProvider { TwoDCodeScanner(instance()) } - bindProvider { TwoDCodeValidator() } - bindSingleton { PrescriptionLocalDataSource(instance()) } - bindSingleton { PrescriptionRemoteDataSource(instance()) } - bindSingleton { PrescriptionUseCase(instance(), instance(), instance()) } - bindSingleton { RefreshPrescriptionUseCase(instance(), instance(), instance(), instance(), instance()) } - bindProvider { GetActivePrescriptionsUseCase(instance()) } - bindProvider { GetArchivedPrescriptionsUseCase(instance()) } - bindProvider { UpdateScannedTaskNameUseCase(instance()) } - bindProvider { GeneratePrescriptionDetailsUseCase(instance()) } -} +val prescriptionModule = + DI.Module("prescriptionModule") { + bindProvider { TwoDCodeProcessor() } + bindProvider { TwoDCodeScanner(instance()) } + bindProvider { TwoDCodeValidator() } + bindSingleton { PrescriptionLocalDataSource(instance()) } + bindSingleton { PrescriptionRemoteDataSource(instance()) } + bindSingleton { PrescriptionUseCase(instance(), instance(), instance()) } + bindSingleton { SaveWelcomeMessageUseCase(instance()) } + bindSingleton { + RefreshPrescriptionUseCase( + instance(), + instance(), + instance(), + instance(), + instance() + ) + } + bindProvider { + DownloadAllResourcesUseCase( + instance(), + instance(), + instance(), + instance(), + instance(), + instance() + ) + } + bindProvider { GetActivePrescriptionsUseCase(instance()) } + bindProvider { GetArchivedPrescriptionsUseCase(instance()) } + bindProvider { DeletePrescriptionUseCase(instance()) } + bindProvider { UpdateScannedTaskNameUseCase(instance()) } + bindProvider { RedeemScannedTaskUseCase(instance()) } + bindProvider { GetPrescriptionByTaskIdUseCase(instance()) } + bindProvider { GetDownloadResourcesDetailStateUseCase(instance()) } + bindProvider { GetDownloadResourcesSnapshotStateUseCase(instance()) } + } -val prescriptionRepositoryModule = DI.Module("prescriptionRepositoryModule", allowSilentOverride = true) { - bindProvider { DefaultPrescriptionRepository(instance(), instance(), instance()) } -} +val prescriptionRepositoryModule = + DI.Module("prescriptionRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultPrescriptionRepository(instance(), instance(), instance()) } + bindSingleton { DownloadResourcesStateRepository() } + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailGraph.kt index ba89e777..133a58cb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.navigation @@ -21,8 +21,15 @@ package de.gematik.ti.erp.app.prescription.detail.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderBottomSheet import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideInRight +import de.gematik.ti.erp.app.navigation.slideOutLeft +import de.gematik.ti.erp.app.navigation.slideOutUp +import de.gematik.ti.erp.app.prescription.detail.ui.HowLongValidBottomSheetScreen import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailAccidentInfoScreen +import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailBottomSheetScreen import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailIngredientsScreen import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailMedicationOverviewScreen import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailMedicationScreen @@ -42,6 +49,9 @@ fun NavGraphBuilder.prescriptionDetailGraph( route = PrescriptionDetailRoutes.subGraphName() ) { renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, route = PrescriptionDetailRoutes.PrescriptionDetailScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailScreen.arguments ) { navEntry -> @@ -51,16 +61,21 @@ fun NavGraphBuilder.prescriptionDetailGraph( ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailMedicationOverviewScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailMedicationOverviewScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailMedicationOverviewScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailMedicationScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailMedicationScreen.arguments ) { navEntry -> @@ -70,64 +85,175 @@ fun NavGraphBuilder.prescriptionDetailGraph( ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailIngredientsScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailIngredientsScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailIngredientsScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailPatientScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailPatientScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailPatientScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailPrescriberScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailPrescriberScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailPrescriberScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailAccidentInfoScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailAccidentInfoScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailAccidentInfoScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailOrganizationScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailOrganizationScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailOrganizationScreen( navController = navController, navBackStackEntry = navEntry ) } renderComposable( + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() }, + popExitAnimation = { slideOutLeft() }, route = PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.route, arguments = PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.arguments - ) { - navEntry -> + ) { navEntry -> PrescriptionDetailTechnicalInformationScreen( navController = navController, navBackStackEntry = navEntry ) } + renderBottomSheet( + route = PrescriptionDetailRoutes.SelPayerPrescriptionBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.SelPayerPrescriptionBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.AdditionalFeeNotExemptBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.AdditionalFeeNotExemptBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.AdditionalFeeExemptBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.AdditionalFeeExemptBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.FailureBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.FailureBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.ScannedBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.ScannedBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.DirectAssignmentBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.DirectAssignmentBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.SubstitutionAllowedBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.SubstitutionAllowedBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.SubstitutionNotAllowedBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.SubstitutionNotAllowedBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.EmergencyFeeExemptBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.EmergencyFeeExemptBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.EmergencyFeeNotExemptBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.EmergencyFeeNotExemptBottomSheetScreen.arguments + ) { navEntry -> + PrescriptionDetailBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionDetailRoutes.HowLongValidBottomSheetScreen.route, + arguments = PrescriptionDetailRoutes.HowLongValidBottomSheetScreen.arguments + ) { navEntry -> + HowLongValidBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailRoutes.kt index 842076f3..c1867eaf 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/navigation/PrescriptionDetailRoutes.kt @@ -1,109 +1,226 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.navigation import androidx.navigation.NavType import androidx.navigation.navArgument -import de.gematik.ti.erp.app.analytics.PopUpName import de.gematik.ti.erp.app.navigation.NavigationRouteNames import de.gematik.ti.erp.app.navigation.NavigationRoutes import de.gematik.ti.erp.app.navigation.Routes object PrescriptionDetailRoutes : NavigationRoutes { override fun subGraphName() = "prescriptionDetail" - const val TaskId = "taskId" - const val SelectedMedication = "selectedMedication" - const val SelectedIngredient = "selectedIngredient" + + const val PRESCRIPTION_DETAIL_NAV_TASK_ID = "taskId" + const val PRESCRIPTION_DETAIL_NAV_SELECTED_MEDICATION = "selectedMedication" + const val PRESCRIPTION_DETAIL_NAV_SELECTED_INGREDIENT = "selectedIngredient" + const val BOTTOM_SHEET_TITLE_ID = "BOTTOM_SHEET_TITLE_ID" + const val BOTTOM_SHEET_INFO_ID = "BOTTOM_SHEET_INFO_ID" + object PrescriptionDetailScreen : Routes( NavigationRouteNames.PrescriptionDetailScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailMedicationOverviewScreen : Routes( NavigationRouteNames.PrescriptionDetailMedicationOverviewScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path( - taskId: String - ) = path( - TaskId to taskId - ) + fun path(taskId: String) = + path( + PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId + ) } + object PrescriptionDetailMedicationScreen : Routes( NavigationRouteNames.PrescriptionDetailMedicationScreen.name, - navArgument(TaskId) { type = NavType.StringType }, - navArgument(SelectedMedication) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType }, + navArgument(PRESCRIPTION_DETAIL_NAV_SELECTED_MEDICATION) { type = NavType.StringType } ) { fun path( taskId: String, selectedMedication: String ) = path( - TaskId to taskId, - SelectedMedication to selectedMedication + PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId, + PRESCRIPTION_DETAIL_NAV_SELECTED_MEDICATION to selectedMedication ) } + object PrescriptionDetailPatientScreen : Routes( NavigationRouteNames.PrescriptionDetailPatientScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailPrescriberScreen : Routes( NavigationRouteNames.PrescriptionDetailPractitionerScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailOrganizationScreen : Routes( NavigationRouteNames.PrescriptionDetailOrganizationScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailAccidentInfoScreen : Routes( NavigationRouteNames.PrescriptionDetailAccidentInfoScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailTechnicalInformationScreen : Routes( NavigationRouteNames.PrescriptionDetailTechnicalInfoScreen.name, - navArgument(TaskId) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } ) { - fun path(taskId: String) = path(TaskId to taskId) + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) } + object PrescriptionDetailIngredientsScreen : Routes( NavigationRouteNames.PrescriptionDetailMedicationIngredientsScreen.name, - navArgument(SelectedIngredient) { type = NavType.StringType } + navArgument(PRESCRIPTION_DETAIL_NAV_SELECTED_INGREDIENT) { type = NavType.StringType } ) { - fun path(selectedIngredient: String) = path(SelectedIngredient to selectedIngredient) + fun path(selectedIngredient: String) = path(PRESCRIPTION_DETAIL_NAV_SELECTED_INGREDIENT to selectedIngredient) + } + + object SelPayerPrescriptionBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailSelfPayerPrescriptionBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object AdditionalFeeNotExemptBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailAdditionalFeeNotExemptBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object AdditionalFeeExemptBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailAdditionalFeeExemptBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object FailureBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailFailureBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object ScannedBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailScannedBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object DirectAssignmentBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailDirectAssignmentBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object SubstitutionAllowedBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailSubstitutionAllowedBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) } -} -object PrescriptionDetailsPopUpNames { - object Validity : PopUpName("prescriptionDetail_prescriptionValidityInfo") - object SubstitutionAllowed : PopUpName("prescriptionDetail_substitutionInfo") - object DirectAssignment : PopUpName("prescriptionDetail_directAssignmentInfo") - object EmergencyFee : PopUpName("prescriptionDetail_emergencyServiceFeeInfo") - object AdditionalFee : PopUpName("prescriptionDetail_coPaymentInfo") - object Scanned : PopUpName("prescriptionDetail_scannedPrescriptionInfo") - object Failure : PopUpName("prescriptionDetail_errorInfo") + object SubstitutionNotAllowedBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailSubstitutionNotAllowedBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object EmergencyFeeExemptBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailEmergencyFeeExemptBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object EmergencyFeeNotExemptBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailEmergencyFeeNotExemptBottomSheetScreen.name, + navArgument(BOTTOM_SHEET_TITLE_ID) { type = NavType.IntType }, + navArgument(BOTTOM_SHEET_INFO_ID) { type = NavType.IntType } + ) { + fun path( + titleId: Int, + infoId: Int + ) = path(BOTTOM_SHEET_TITLE_ID to titleId, BOTTOM_SHEET_INFO_ID to infoId) + } + + object HowLongValidBottomSheetScreen : Routes( + NavigationRouteNames.PrescriptionDetailHowLongValidBottomSheetScreen.name, + navArgument(PRESCRIPTION_DETAIL_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String) = path(PRESCRIPTION_DETAIL_NAV_TASK_ID to taskId) + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskIdController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskIdController.kt new file mode 100644 index 00000000..aeb687c9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskIdController.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +open class GetPrescriptionByTaskIdController( + private val taskId: String, + private val getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase +) : Controller() { + private val _prescription = MutableStateFlow>(UiState.Loading()) + val prescription: StateFlow> = _prescription + + init { + initPrescription() + } + + private fun initPrescription() { + controllerScope.launch { + runCatching { + getPrescriptionByTaskIdUseCase(taskId).first() + }.onSuccess { + _prescription.value = UiState.Data(it) + }.onFailure { + _prescription.value = UiState.Error(it) + } + } + } +} + +@Composable +fun rememberGetPrescriptionByTaskIdController( + taskId: String +): GetPrescriptionByTaskIdController { + val getPrescriptionByTaskIdUseCase by rememberInstance() + return remember { + GetPrescriptionByTaskIdController( + taskId = taskId, + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailController.kt index ad16119a..7b3f239f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.presentation @@ -21,89 +21,150 @@ package de.gematik.ti.erp.app.prescription.detail.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.consent.usecase.GrantConsentUseCase -import de.gematik.ti.erp.app.prescription.detail.ui.DeletePrescriptionsBridge -import de.gematik.ti.erp.app.prescription.usecase.GeneratePrescriptionDetailsUseCase -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.usecase.LoadMedicationScheduleByTaskIdUseCase +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.usecase.DeletePrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.prescription.usecase.RedeemScannedTaskUseCase import de.gematik.ti.erp.app.prescription.usecase.UpdateScannedTaskNameUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase -import kotlinx.coroutines.CoroutineScope +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance +@Suppress("ConstructorParameterNaming") @Stable class PrescriptionDetailController( + getActiveProfileUseCase: GetActiveProfileUseCase, + featureToggleManager: FeatureToggleManager, private val taskId: String, - private val prescriptionUseCase: PrescriptionUseCase, - private val activeProfileUseCase: GetActiveProfileUseCase, - private val grantConsentUseCase: GrantConsentUseCase, - private val generatePrescriptionDetailsUseCase: GeneratePrescriptionDetailsUseCase, + private val redeemScannedTaskUseCase: RedeemScannedTaskUseCase, + private val deletePrescriptionUseCase: DeletePrescriptionUseCase, + private val loadMedicationScheduleByTaskIdUseCase: LoadMedicationScheduleByTaskIdUseCase, + private val getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase, private val updateScannedTaskNameUseCase: UpdateScannedTaskNameUseCase, - private val scope: CoroutineScope -) : DeletePrescriptionsBridge { + private val _profilePrescription: + MutableStateFlow>> = + MutableStateFlow(UiState.Loading()), + val profilePrescription: StateFlow>> = + _profilePrescription +) : GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = { profile, scope -> + scope.launch { + runCatching { + getPrescriptionByTaskIdUseCase(taskId).first() + }.fold( + onSuccess = { + _profilePrescription.value = UiState.Data(profile to it) + }, + onFailure = { + _profilePrescription.value = UiState.Error(it) + } + ) + } + }, + onFailure = { error, _ -> + _profilePrescription.value = UiState.Error(error) + } +) { + + val medicationSchedule: StateFlow = loadMedicationScheduleByTaskIdUseCase(taskId) + .stateIn(controllerScope, SharingStarted.WhileSubscribed(), null) - private val prescription by lazy { - generatePrescriptionDetailsUseCase(taskId).stateIn(scope, SharingStarted.Lazily, null) + private val _prescriptionDeleted by lazy { + MutableStateFlow( + DeletePrescriptionUseCase.DeletePrescriptionState.ValidState.NotDeleted + ) } - private val activeProfile by lazy { - activeProfileUseCase.invoke() + val prescriptionDeleted: StateFlow by lazy { + _prescriptionDeleted } - fun redeemScannedTask(taskId: String, redeem: Boolean) { - scope.launch { - prescriptionUseCase.redeemScannedTask(taskId, redeem) + val isMedicationPlanEnabled: StateFlow = + featureToggleManager.isFeatureEnabled(Features.MEDICATION_PLAN) + .stateIn( + controllerScope, + SharingStarted.WhileSubscribed(), + false + ) + + fun redeemScannedTask( + taskId: String, + redeem: Boolean + ) { + controllerScope.launch { + redeemScannedTaskUseCase(taskId, redeem) + refreshActiveProfile() } } - fun updateScannedTaskName(taskId: String, name: String) { - scope.launch { - updateScannedTaskNameUseCase.invoke(taskId, name) + fun updateScannedTaskName( + taskId: String, + name: String + ) { + controllerScope.launch { + updateScannedTaskNameUseCase(taskId, name) + refreshActiveProfile() } } - override suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result = - prescriptionUseCase.deletePrescription(profileId = profileId, taskId = taskId) - - fun grantConsent() { - scope.launch { - val profile = activeProfile.first() - grantConsentUseCase.invoke(profile.id, profile.insurance.insuranceIdentifier) + fun deletePrescription( + profileId: ProfileIdentifier, + taskId: String + ) = controllerScope.launch { + deletePrescriptionUseCase(profileId, taskId, false).first().apply { + _prescriptionDeleted.value = this as DeletePrescriptionUseCase.DeletePrescriptionState } } - val prescriptionState - @Composable - get() = prescription.collectAsStateWithLifecycle() + fun deletePrescriptionFromLocal( + profileId: ProfileIdentifier, + taskId: String + ) { + controllerScope.launch { + deletePrescriptionUseCase(profileId, taskId, true).first().apply { + _prescriptionDeleted.value = this as DeletePrescriptionUseCase.DeletePrescriptionState + } + } + } - val activeProfileState - @Composable - get() = activeProfile.collectAsStateWithLifecycle(initialValue = null) + fun resetDeletePrescriptionState() { + _prescriptionDeleted.value = DeletePrescriptionUseCase.DeletePrescriptionState.ValidState.NotDeleted + } } @Composable fun rememberPrescriptionDetailController(taskId: String): PrescriptionDetailController { - val generatePrescriptionDetailsUseCase by rememberInstance() - val prescriptionUseCase by rememberInstance() + val getPrescriptionByTaskIdUseCase by rememberInstance() + val redeemScannedTaskUseCase by rememberInstance() + val deletePrescriptionUseCase by rememberInstance() + val loadMedicationScheduleByTaskIdUseCase by rememberInstance() val updateScannedTaskNameUseCase by rememberInstance() - val activeProfileUseCase by rememberInstance() - val grantConsentUseCase by rememberInstance() - val scope = rememberCoroutineScope() + val getActiveProfileUseCase by rememberInstance() + val featureToggleManager by rememberInstance() return remember { PrescriptionDetailController( taskId = taskId, - generatePrescriptionDetailsUseCase = generatePrescriptionDetailsUseCase, - prescriptionUseCase = prescriptionUseCase, + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase, + redeemScannedTaskUseCase = redeemScannedTaskUseCase, + deletePrescriptionUseCase = deletePrescriptionUseCase, + loadMedicationScheduleByTaskIdUseCase = loadMedicationScheduleByTaskIdUseCase, updateScannedTaskNameUseCase = updateScannedTaskNameUseCase, - activeProfileUseCase = activeProfileUseCase, - grantConsentUseCase = grantConsentUseCase, - scope = scope + getActiveProfileUseCase = getActiveProfileUseCase, + featureToggleManager = featureToggleManager ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/SharePrescriptionController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/SharePrescriptionController.kt index 60bf6d0f..853755a4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/SharePrescriptionController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/SharePrescriptionController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.presentation @@ -26,6 +26,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.core.LocalIntentHandler import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.model.ScannedTaskData @@ -77,6 +78,11 @@ class SharePrescriptionController( * * URI pattern is .../prescription/#["TASK_ID|ACCESS_CODE"] */ + @Requirement( + "O.Source_1#9", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Uri is validated for task-id and access-code pattern before sharing" + ) suspend fun handle(value: String): HandleResult = if (value.startsWith(ShareBaseUri)) { try { @@ -100,13 +106,14 @@ class SharePrescriptionController( ScannedTaskData.ScannedTask( profileId = profileId, index = 0, - name = null, + name = "", // name will be set later taskId = taskId, accessCode = accessCode, scannedOn = Clock.System.now(), redeemedOn = null ) - ) + ), + medicationString = context.getString(R.string.pres_details_scanned_medication) ) HandleResult.TaskSaved diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt deleted file mode 100644 index 6d072cb2..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.detail.ui - -import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions -import de.gematik.ti.erp.app.prescription.presentation.retryWithAuthenticator -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import java.net.HttpURLConnection - -interface DeletePrescriptionsBridge { - suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result -} - -@Stable -class DeletePrescriptions( - private val prescriptionDetailsController: DeletePrescriptionsBridge, - private val authenticator: Authenticator -) { - sealed interface State : PrescriptionServiceState { - object Deleted : State - - sealed interface Error : State, PrescriptionServiceErrorState { - object PrescriptionWorkflowBlocked : Error - object PrescriptionNotFound : Error - } - } - - @Requirement( - "A_19229#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "User can delete a locally and remotely stored prescription and all its linked resources." - ) - suspend fun deletePrescription( - profileId: ProfileIdentifier, - taskId: String - ): PrescriptionServiceState = - deletePrescriptionFlow(profileId = profileId, taskId = taskId).cancellable().first() - - private fun deletePrescriptionFlow(profileId: ProfileIdentifier, taskId: String) = - flow { - emit(prescriptionDetailsController.deletePrescription(profileId = profileId, taskId = taskId)) - }.map { result -> - result.fold( - onSuccess = { - State.Deleted - }, - onFailure = { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_FORBIDDEN -> State.Error.PrescriptionWorkflowBlocked - HttpURLConnection.HTTP_GONE, - HttpURLConnection.HTTP_NOT_FOUND -> State.Error.PrescriptionNotFound - - else -> throw it - } - } else { - throw it - } - } - ) - } - .retryWithAuthenticator( - isUserAction = true, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt deleted file mode 100644 index 5fc8277d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.detail.ui - -import android.content.Context -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState - -fun deleteErrorMessage(context: Context, deleteState: PrescriptionServiceErrorState): String? = - when (deleteState) { - GeneralErrorState.NetworkNotAvailable -> - context.getString(R.string.error_message_network_not_available) - is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - context.getString(R.string.error_message_server_communication_failed).format(deleteState.code) - GeneralErrorState.FatalTruststoreState -> - context.getString(R.string.error_message_vau_error) - is DeletePrescriptions.State.Error.PrescriptionWorkflowBlocked -> - context.getString(R.string.logout_delete_in_progress) - is DeletePrescriptions.State.Error.PrescriptionNotFound -> - context.getString(R.string.prescription_not_found) - is GeneralErrorState.NoneEnrolled -> - context.getString(R.string.no_auth_enrolled) - else -> null - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HandleDeletePrescriptionState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HandleDeletePrescriptionState.kt new file mode 100644 index 00000000..7fcef2eb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HandleDeletePrescriptionState.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import android.app.Dialog +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.api.HTTP_BAD_REQUEST +import de.gematik.ti.erp.app.api.HTTP_METHOD_NOT_ALLOWED +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.usecase.DeletePrescriptionUseCase +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Suppress("FunctionNaming", "LongParameterList", "LongMethod") +@Composable +fun HandleDeletePrescriptionState( + state: DeletePrescriptionUseCase.DeletePrescriptionState, + dialog: DialogScaffold, + onConfirmDialogRequest: ( + sendFeedBackMessage: Pair, + deletePrescriptionLocally: Boolean + ) -> Unit, + onShowCardWall: () -> Unit, + onRetry: () -> Unit, + onDismiss: () -> Unit, + onBack: () -> Unit +) { + var sendFeedBackMessageSelected by remember { mutableStateOf(true) } + var deletePrescriptionLocallySelected by remember { mutableStateOf(false) } + + LaunchedEffect(state) { + when (state) { + is DeletePrescriptionUseCase.DeletePrescriptionState.ValidState.Deleted -> { + onBack() + } + + is DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.BadRequest -> { + dialog.show { dialog -> + BadRequestErrorDialog( + sendFeedBackMessageSelected = sendFeedBackMessageSelected, + deletePrescriptionLocallySelected = deletePrescriptionLocallySelected, + dialog = dialog, + errorCode = HTTP_BAD_REQUEST, + onConfirmDialogRequest = onConfirmDialogRequest, + onClickSendFeedBackMessage = { selected -> + sendFeedBackMessageSelected = selected + } + ) { selected -> + deletePrescriptionLocallySelected = selected + } + } + } + + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.InternalError -> { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.delete_prescription_error_internal_title), + body = stringResource(R.string.delete_prescription_error_internal_info), + onDismissRequest = { + onDismiss() + dialog.dismiss() + } + ) + } + } + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.MethodNotAllowed -> { + dialog.show { dialog -> + BadRequestErrorDialog( + sendFeedBackMessageSelected = sendFeedBackMessageSelected, + deletePrescriptionLocallySelected = deletePrescriptionLocallySelected, + dialog = dialog, + errorCode = HTTP_METHOD_NOT_ALLOWED, + onConfirmDialogRequest = onConfirmDialogRequest, + onClickSendFeedBackMessage = { selected -> + sendFeedBackMessageSelected = selected + } + ) { selected -> + deletePrescriptionLocallySelected = selected + } + } + } + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.NoInternet -> { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.delete_prescription_error_no_internet_title), + bodyText = stringResource(R.string.delete_prescription_error_no_internet_info), + confirmText = stringResource(R.string.delete_prescription_error_no_internet_confirm), + dismissText = stringResource(R.string.delete_prescription_error_no_internet_dissmiss), + onDismissRequest = { + onDismiss() + dialog.dismiss() + }, + onConfirmRequest = { + onRetry() + dialog.dismiss() + } + ) + } + } + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.ErpWorkflowBlocked -> { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.delete_prescription_error_blocked_title), + body = stringResource(R.string.delete_prescription_error_blocked_info), + onDismissRequest = { + onDismiss() + dialog.dismiss() + } + ) + } + } + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.TooManyRequests -> { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.delete_prescription_error_too_many_requests_title), + body = stringResource(R.string.delete_prescription_error_too_many_requests_info), + onDismissRequest = { + onDismiss() + dialog.dismiss() + } + ) + } + } + DeletePrescriptionUseCase.DeletePrescriptionState.ErrorState.Unauthorized -> { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.delete_prescription_error_unauthorized_title), + bodyText = stringResource(R.string.delete_prescription_error_unauthorized_info), + confirmText = stringResource(R.string.delete_prescription_error_unauthorized_confirm), + dismissText = stringResource(R.string.delete_prescription_error_unauthorized_dissmiss), + onDismissRequest = { + onDismiss() + dialog.dismiss() + }, + onConfirmRequest = { + onShowCardWall() + dialog.dismiss() + } + ) + } + } + else -> {} + } + } +} + +@Suppress("FunctionNaming", "LongParameterList") +@Composable +private fun BadRequestErrorDialog( + sendFeedBackMessageSelected: Boolean, + deletePrescriptionLocallySelected: Boolean, + dialog: Dialog, + errorCode: Int, + onConfirmDialogRequest: ( + sendFeedBackMessage: Pair, + deletePrescriptionLocally: Boolean + ) -> Unit, + onClickSendFeedBackMessage: (Boolean) -> Unit, + onClickDeletePrescriptionLocally: (Boolean) -> Unit +) { + val errorMessage = stringResource(R.string.delete_prescription_feedback_mail_body, errorCode) + + ErezeptAlertDialog( + title = stringResource(R.string.delete_error_dialog_bad_request_title), + body = { + BadRequestErrorBody( + errorNumber = errorCode, + sendFeedBackMessageSelected, + deletePrescriptionLocallySelected, + onClickSendFeedBackMessage = { selected -> + onClickSendFeedBackMessage(selected) + }, + onClickDeletePrescriptionLocally = { selected -> + onClickDeletePrescriptionLocally(selected) + } + ) + }, + okText = stringResource(R.string.delete_error_dialog_forward), + onDismissRequest = { dialog.dismiss() }, + onConfirmRequest = { + onConfirmDialogRequest( + Pair(sendFeedBackMessageSelected, errorMessage), + deletePrescriptionLocallySelected + ) + dialog.dismiss() + } + ) +} + +@Suppress("FunctionNaming") +@Composable +private fun BadRequestErrorBody( + errorNumber: Int, + sendFeedBackMessageSelected: Boolean, + deletePrescriptionLocallySelected: Boolean, + onClickSendFeedBackMessage: (Boolean) -> Unit, + onClickDeletePrescriptionLocally: (Boolean) -> Unit +) { + Column { + Text( + annotatedStringResource(R.string.delete_prescription_bad_request_info, errorNumber).toString() + ) + SpacerMedium() + + LabeledCheckBox( + checked = sendFeedBackMessageSelected, + labelText = stringResource(R.string.delete_prescription_send_support_message), + onCheckedChanged = { + onClickSendFeedBackMessage(it) + } + ) + LabeledCheckBox( + checked = deletePrescriptionLocallySelected, + labelText = stringResource(R.string.delete_prescription_locally), + onCheckedChanged = { + onClickDeletePrescriptionLocally(it) + } + ) + } +} + +@Suppress("FunctionNaming") +@Composable +private fun LabeledCheckBox( + checked: Boolean, + labelText: String, + onCheckedChanged: (Boolean) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .clickable + { onCheckedChanged(!checked) } + ) { + Checkbox( + modifier = Modifier.padding(start = PaddingDefaults.ShortMedium), + checked = checked, + onCheckedChange = { onCheckedChanged(it) } + ) + Text(labelText) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HowLongValidBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HowLongValidBottomSheetScreen.kt new file mode 100644 index 00000000..f9407569 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/HowLongValidBottomSheetScreen.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowForward +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.prescription.detail.presentation.rememberGetPrescriptionByTaskIdController +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.extensions.dateTimeMediumText +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.days + +class HowLongValidBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = false) { + @Composable + override fun Content() { + val taskId = remember { + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID + ) + } ?: "" + + val controller = rememberGetPrescriptionByTaskIdController(taskId) + val prescriptionState by controller.prescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = prescriptionState, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { content -> + val prescription = content as PrescriptionData.Synced + Column( + Modifier + .padding(horizontal = PaddingDefaults.Medium) + .padding(top = PaddingDefaults.Small, bottom = PaddingDefaults.XXLarge) + ) { + SpacerMedium() + Text( + stringResource(R.string.pres_details_exp_valid_title), + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral900, + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriptionDetailBottomSheetTitle) + ) + SpacerMedium() + val start = + if (prescription.medicationRequest.multiplePrescriptionInfo.indicator) { + prescription.medicationRequest.multiplePrescriptionInfo.start + ?: prescription.authoredOn + } else { + prescription.authoredOn + } + Column { + DateRange(start = start, end = prescription.acceptUntil?.minus(1.days) ?: start) + SpacerSmall() + Text( + stringResource(R.string.pres_details_exp_valid_info_accept), + style = AppTheme.typography.body2l + ) + if (!prescription.medicationRequest.multiplePrescriptionInfo.indicator) { + SpacerMedium() + DateRange( + start = + remember { + prescription.acceptUntil ?: start + }, + end = prescription.expiresOn?.minus(1.days) ?: start + ) + SpacerSmall() + Text( + stringResource(R.string.pres_details_exp_valid_info_expiry_time), + style = AppTheme.typography.body2l + ) + } + } + } + } + ) + } + + @Composable + private fun DateRange( + start: Instant, + end: Instant + ) { + val startText = remember { dateTimeMediumText(start) } + val endText = remember { dateTimeMediumText(end) } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + androidx.compose.material.Text(startText, style = AppTheme.typography.subtitle2l) + androidx.compose.material.Icon( + Icons.AutoMirrored.Rounded.ArrowForward, + null, + tint = AppTheme.colors.primary600, + modifier = Modifier.size(SizeDefaults.double) + ) + androidx.compose.material.Text(endText, style = AppTheme.typography.subtitle2l) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt deleted file mode 100644 index 11ee3e56..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.detail.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowForward -import androidx.compose.material.icons.rounded.DragHandle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailsPopUpNames -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.extensions.dateTimeMediumText -import kotlinx.datetime.Instant -import kotlin.time.Duration.Companion.days - -sealed class PrescriptionDetailBottomSheetContent { - @Stable - class HowLongValid( - val prescription: PrescriptionData.Synced, - val popUp: PrescriptionDetailsPopUpNames.Validity = PrescriptionDetailsPopUpNames.Validity - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class SubstitutionAllowed( - val popUp: PrescriptionDetailsPopUpNames.SubstitutionAllowed = PrescriptionDetailsPopUpNames.SubstitutionAllowed - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class DirectAssignment( - val popUp: PrescriptionDetailsPopUpNames.DirectAssignment = PrescriptionDetailsPopUpNames.DirectAssignment - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class EmergencyFee( - val popUp: PrescriptionDetailsPopUpNames.EmergencyFee = PrescriptionDetailsPopUpNames.EmergencyFee - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class EmergencyFeeNotExempt( - val popUp: PrescriptionDetailsPopUpNames.EmergencyFee = PrescriptionDetailsPopUpNames.EmergencyFee - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class AdditionalFeeNotExempt( - val popUp: PrescriptionDetailsPopUpNames.AdditionalFee = PrescriptionDetailsPopUpNames.AdditionalFee - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class AdditionalFeeExempt( - val popUp: PrescriptionDetailsPopUpNames.AdditionalFee = PrescriptionDetailsPopUpNames.AdditionalFee - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class Scanned( - val popUp: PrescriptionDetailsPopUpNames.Scanned = PrescriptionDetailsPopUpNames.Scanned - ) : - PrescriptionDetailBottomSheetContent() - - @Stable - class Failure( - val popUp: PrescriptionDetailsPopUpNames.Failure = PrescriptionDetailsPopUpNames.Failure - ) : - PrescriptionDetailBottomSheetContent() -} - -@Composable -fun PrescriptionDetailInfoSheetContent( - infoContent: PrescriptionDetailBottomSheetContent -) { - when (infoContent) { - is PrescriptionDetailBottomSheetContent.DirectAssignment -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_da_title), - info = stringResource(R.string.pres_details_exp_da_info) - ) - - is PrescriptionDetailBottomSheetContent.EmergencyFee -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_em_fee_title), - info = stringResource(R.string.pres_details_exp_em_fee_info) - ) - - is PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_no_em_fee_title), - info = stringResource(R.string.pres_details_exp_no_em_fee_info) - ) - - is PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_add_fee_title), - info = stringResource(R.string.pres_details_exp_add_fee_info) - ) - - is PrescriptionDetailBottomSheetContent.AdditionalFeeExempt -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_no_add_fee_title), - info = stringResource(R.string.pres_details_exp_no_add_fee_info) - ) - - is PrescriptionDetailBottomSheetContent.HowLongValid -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_valid_title) - ) { - val start = if (infoContent.prescription.medicationRequest.multiplePrescriptionInfo.indicator) { - infoContent.prescription.medicationRequest.multiplePrescriptionInfo.start - ?: infoContent.prescription.authoredOn - } else { - infoContent.prescription.authoredOn - } - Column { - DateRange(start = start, end = infoContent.prescription.acceptUntil?.minus(1.days) ?: start) - SpacerSmall() - Text( - stringResource(R.string.pres_details_exp_valid_info_accept), - style = AppTheme.typography.body2l - ) - if (!infoContent.prescription.medicationRequest.multiplePrescriptionInfo.indicator) { - SpacerMedium() - DateRange( - start = remember { - infoContent.prescription.acceptUntil ?: start - }, - end = infoContent.prescription.expiresOn?.minus(1.days) ?: start - ) - SpacerSmall() - Text( - stringResource(R.string.pres_details_exp_valid_info_expiry_time), - style = AppTheme.typography.body2l - ) - } - } - } - - is PrescriptionDetailBottomSheetContent.SubstitutionAllowed -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_sub_allowed_title), - info = stringResource(R.string.pres_details_exp_sub_allowed_info) - ) - - is PrescriptionDetailBottomSheetContent.Scanned -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_scanned_title), - info = stringResource(R.string.pres_details_exp_scanned_info) - ) - - is PrescriptionDetailBottomSheetContent.Failure -> - PrescriptionDetailInfoSheetContent( - title = stringResource(R.string.pres_details_exp_failure_title), - info = stringResource(R.string.pres_details_exp_failure_info) - ) - } -} - -@Composable -private fun DateRange(start: Instant, end: Instant) { - val startText = remember { dateTimeMediumText(start) } - val endText = remember { dateTimeMediumText(end) } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - Text(startText, style = AppTheme.typography.subtitle2l) - Icon(Icons.Rounded.ArrowForward, null, tint = AppTheme.colors.primary600, modifier = Modifier.size(16.dp)) - Text(endText, style = AppTheme.typography.subtitle2l) - } -} - -@Composable -private fun PrescriptionDetailInfoSheetContent( - title: String, - info: String -) { - PrescriptionDetailInfoSheetContent( - title = title - ) { - Text(info, style = AppTheme.typography.body2l) - } -} - -@Composable -private fun PrescriptionDetailInfoSheetContent( - title: String, - content: @Composable () -> Unit -) { - Column( - Modifier - .padding(horizontal = PaddingDefaults.Medium) - .padding(top = PaddingDefaults.Small, bottom = PaddingDefaults.XXLarge) - ) { - Icon( - Icons.Rounded.DragHandle, - null, - tint = AppTheme.colors.neutral600, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - SpacerMedium() - Text(title, style = AppTheme.typography.subtitle1) - SpacerMedium() - Box(Modifier.verticalScroll(rememberScrollState())) { - content() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceCard.kt index 33fa3b0c..f3f6ee5f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceCard.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -36,7 +36,7 @@ import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowForward +import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.rounded.ModelTraining import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerSmall @Composable fun InvoiceCard( @@ -119,7 +119,7 @@ fun NoConsentGrantedCard( ) SpacerSmall() Icon( - Icons.Rounded.ArrowForward, + Icons.AutoMirrored.Rounded.ArrowForward, null ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceLoadingCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceLoadingCard.kt index 1ffdbd62..b85739df 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceLoadingCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InvoiceLoadingCard.kt @@ -1,55 +1,45 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.valentinilk.shimmer.shimmer import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.CircularShapeShimmer import de.gematik.ti.erp.app.utils.compose.LightDarkPreview -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -private const val LIGHT_ALPHA = 0.2F +import de.gematik.ti.erp.app.utils.compose.LimitedTextShimmer +import de.gematik.ti.erp.app.utils.compose.RowTextShimmer +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable fun InvoiceLoadingCard() { @@ -70,58 +60,19 @@ fun InvoiceLoadingCard() { horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), verticalAlignment = Alignment.Top ) { - CircularShape() + CircularShapeShimmer() Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Tiny) ) { - TitleRow() + LimitedTextShimmer() SpacerSmall() - BodyRow() + RowTextShimmer() } } } } -@Composable -private fun CircularShape( - background: Color = AppTheme.colors.primary200, - alpha: Float = LIGHT_ALPHA, - size: Dp = 40.dp -) { - Box( - modifier = Modifier - .size(size) - .clip(CircleShape) - .background(background) - .alpha(alpha) - ) -} - -@Composable -private fun TitleRow( - background: Color = AppTheme.colors.primary200 -) { - Spacer( - modifier = Modifier - .height(12.dp) - .width(120.dp) - .background(background) - ) -} - -@Composable -private fun BodyRow( - background: Color = AppTheme.colors.primary200 -) { - Spacer( - modifier = Modifier - .height(12.dp) - .fillMaxWidth() - .background(background) - ) -} - @LightDarkPreview @Composable fun InvoiceShimmerCardPreview() { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreen.kt index 43339b80..d6177ab5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -28,31 +28,39 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController +import de.gematik.ti.erp.app.core.LocalTimeZone import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.preview.AccidentInfoPreviewParameter +import de.gematik.ti.erp.app.prescription.detail.ui.preview.AccidentInfoPreviewParameterProvider +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDate +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.dateString +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle private const val NoInfo = "-" + class PrescriptionDetailAccidentInfoScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry @@ -60,33 +68,58 @@ class PrescriptionDetailAccidentInfoScreen( @Composable override fun Content() { val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID ) - } + } ?: "" + val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced - // TODO : UI for accident types - val isAccident = remember(syncedPrescription) { - syncedPrescription?.medicationRequest?.accidentType != SyncedTaskData.AccidentType.None - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() - val listState = rememberLazyListState() - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.pres_detail_accident_header), - listState = listState, - onBack = navController::popBackStack, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - PrescriptionDetailAccidentInfoScreenContent( - listState, - innerPadding, - isAccident, - syncedPrescription - ) - } + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val listState = rememberLazyListState() + PrescriptionDetailAccidentInfoScreenScaffold( + listState = listState, + syncedPrescription = syncedPrescription, + onBack = navController::popBackStack + ) + } + ) + } +} + +@Composable +private fun PrescriptionDetailAccidentInfoScreenScaffold( + listState: LazyListState, + syncedPrescription: PrescriptionData.Synced?, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.pres_detail_accident_title), + listState = listState, + onBack = onBack, + navigationMode = NavigationBarMode.Back + ) { innerPadding -> + PrescriptionDetailAccidentInfoScreenContent( + listState, + innerPadding, + syncedPrescription + ) } } @@ -94,7 +127,6 @@ class PrescriptionDetailAccidentInfoScreen( private fun PrescriptionDetailAccidentInfoScreenContent( listState: LazyListState, innerPadding: PaddingValues, - isAccident: Boolean, syncedPrescription: PrescriptionData.Synced? ) { LazyColumn( @@ -104,40 +136,41 @@ private fun PrescriptionDetailAccidentInfoScreenContent( ) { item { SpacerMedium() - Label( - text = if (isAccident) { - stringResource(R.string.pres_detail_yes) - } else { - stringResource(R.string.pres_detail_no) - }, - label = stringResource(R.string.pres_detail_accident_header) - ) + if (syncedPrescription != null) { + Label( + text = + when (syncedPrescription.medicationRequest.accidentType) { + SyncedTaskData.AccidentType.Unfall -> stringResource(id = R.string.pres_detail_accident_type_accident) + SyncedTaskData.AccidentType.Arbeitsunfall -> stringResource(id = R.string.pres_detail_accident_type_work_accident) + SyncedTaskData.AccidentType.Berufskrankheit -> stringResource(id = R.string.pres_detail_accident_type_occupational_illness) + else -> NoInfo + }, + label = stringResource(R.string.pres_detail_accident_title) + ) + } } item { - val text = if (isAccident) { - remember(LocalConfiguration.current, syncedPrescription?.medicationRequest?.dateOfAccident) { - val dtFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - syncedPrescription?.medicationRequest?.dateOfAccident - ?.toLocalDateTime(TimeZone.currentSystemDefault()) - ?.date - ?.toJavaLocalDate() - ?.format(dtFormatter) - ?: MissingValue + val timeZone = LocalTimeZone.current + val text = + remember(syncedPrescription?.medicationRequest?.dateOfAccident) { + syncedPrescription + ?.medicationRequest + ?.dateOfAccident + ?.toLocalDateTime(timeZone)?.let { + dateString( + it + ) + } ?: NoInfo } - } else { - NoInfo - } + Label( text = text, - label = stringResource(R.string.pres_detail_accident_label_date) + label = stringResource(R.string.pres_detail_accident_date_label) ) } item { - val text = if (isAccident) { - syncedPrescription?.medicationRequest?.location ?: MissingValue - } else { - NoInfo - } + val text = syncedPrescription?.medicationRequest?.location ?: NoInfo + Label( text = text, label = stringResource(R.string.pres_detail_accident_label_location) @@ -146,3 +179,18 @@ private fun PrescriptionDetailAccidentInfoScreenContent( } } } + +@LightDarkPreview +@Composable +fun PrescriptionDetailAccidentInfoScreenPreview( + @PreviewParameter(AccidentInfoPreviewParameterProvider::class) accidentInfoPreviewParameter: AccidentInfoPreviewParameter +) { + val listState = rememberLazyListState() + PreviewAppTheme { + PrescriptionDetailAccidentInfoScreenScaffold( + listState = listState, + syncedPrescription = accidentInfoPreviewParameter.syncedPrescription, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailBottomSheetScreen.kt new file mode 100644 index 00000000..97d23502 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailBottomSheetScreen.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium + +class PrescriptionDetailBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = false) { + @Composable + override fun Content() { + val titleId = + remember { + navBackStackEntry.arguments?.getInt(PrescriptionDetailRoutes.BOTTOM_SHEET_TITLE_ID) + } ?: return + val infoId = + remember { + navBackStackEntry.arguments?.getInt(PrescriptionDetailRoutes.BOTTOM_SHEET_INFO_ID) + } ?: return + + Column( + Modifier + .padding(horizontal = PaddingDefaults.Medium) + .padding(top = PaddingDefaults.Small, bottom = PaddingDefaults.XXLarge) + ) { + SpacerMedium() + Text( + stringResource(id = titleId), + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.neutral900, + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriptionDetailBottomSheetTitle) + ) + SpacerMedium() + Box( + Modifier + .verticalScroll( + rememberScrollState() + ).testTag(TestTag.Prescriptions.Details.PrescriptionDetailBottomSheetDetail) + ) { + Text( + stringResource(id = infoId), + style = AppTheme.typography.body2l + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailIngredientsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailIngredientsScreen.kt index 166dd045..60b50591 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailIngredientsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailIngredientsScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -43,13 +43,15 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.navigation.fromNavigationString import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.annotatedStringResource class PrescriptionDetailIngredientsScreen( @@ -60,7 +62,9 @@ class PrescriptionDetailIngredientsScreen( override fun Content() { val selectedIngredient = remember { requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.SelectedIngredient) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_SELECTED_INGREDIENT + ) ) } val ingredient = remember(selectedIngredient) { @@ -158,7 +162,7 @@ fun IngredientNameLabel(text: String, index: Int? = null, onClickLabel: (() -> U } @Composable -private fun StrengthLabel(strength: SyncedTaskData.Ratio) { +private fun StrengthLabel(strength: Ratio) { strength.numerator?.let { Label( text = it.value + " " + it.unit, @@ -178,9 +182,9 @@ private fun PrescriptionDetailIngredientsScreenContentPreview() { form = "some form text", number = "5", amount = "20 ml", - strength = SyncedTaskData.Ratio( - SyncedTaskData.Quantity("10", "ng"), - SyncedTaskData.Quantity("10", "ml") + strength = Ratio( + Quantity("10", "ng"), + Quantity("10", "ml") ) ) PrescriptionDetailIngredientsScreenContent( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationOverviewScreen.kt index 08027770..bc94a322 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationOverviewScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationOverviewScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.SnackbarHost import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState @@ -38,6 +39,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.features.R @@ -45,15 +47,18 @@ import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.navigation.toNavigationString import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center class PrescriptionDetailMedicationOverviewScreen( override val navController: NavController, @@ -61,33 +66,55 @@ class PrescriptionDetailMedicationOverviewScreen( ) : Screen() { @Composable override fun Content() { - val taskId = - remember { requireNotNull(navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId)) } + val taskId = remember { + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID + ) + } ?: "" + val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val scaffoldState = rememberScaffoldState() - val listState = rememberLazyListState() - AnimatedElevationScaffold( - scaffoldState = scaffoldState, - listState = listState, - onBack = navController::popBackStack, - topBarTitle = stringResource(R.string.synced_medication_detail_header), - navigationMode = NavigationBarMode.Back, - snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, - actions = {} - ) { innerPadding -> - val syncedPrescription = (prescription as? PrescriptionData.Synced) - syncedPrescription?.medicationRequest?.medication?.let { medication -> - PrescriptionDetailMedicationOverviewScreenContent( - listState, - innerPadding, - navController, - medication, - syncedPrescription, - taskId - ) + + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val scaffoldState = rememberScaffoldState() + val listState = rememberLazyListState() + AnimatedElevationScaffold( + scaffoldState = scaffoldState, + listState = listState, + onBack = navController::popBackStack, + topBarTitle = stringResource(R.string.synced_medication_detail_header), + navigationMode = NavigationBarMode.Back, + snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, + actions = {} + ) { innerPadding -> + syncedPrescription?.medicationRequest?.medication?.let { medication -> + PrescriptionDetailMedicationOverviewScreenContent( + listState, + innerPadding, + navController, + medication, + syncedPrescription, + taskId + ) + } + } } - } + ) } } @@ -102,7 +129,8 @@ private fun PrescriptionDetailMedicationOverviewScreenContent( ) { LazyColumn( state = listState, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(innerPadding), contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() @@ -123,7 +151,8 @@ private fun PrescriptionDetailMedicationOverviewScreenContent( PrescriptionDetailRoutes.PrescriptionDetailMedicationScreen.path( taskId = taskId, selectedMedication = - PrescriptionData.Medication.Request(syncedPrescription.medicationRequest) + PrescriptionData.Medication + .Request(syncedPrescription.medicationRequest) .toNavigationString() ) ) @@ -142,10 +171,10 @@ private fun PrescriptionDetailMedicationOverviewScreenContent( syncedPrescription.medicationDispenses.forEach { dispense -> // TODO: add tracking event (with dispenseId + performer) in case of medication is null - dispense.medication?.let { + dispense.medication?.let { medication -> item { Label( - text = it.name(), + text = medication.name(), label = null, onClick = { navController.navigate( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationScreen.kt index fda514c5..de660564 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailMedicationScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("TooManyFunctions") @@ -33,6 +33,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.SnackbarHost import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable @@ -42,6 +43,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.BuildKonfig @@ -53,25 +55,29 @@ import de.gematik.ti.erp.app.navigation.fromNavigationString import de.gematik.ti.erp.app.navigation.toNavigationString import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.repository.codeToFormMapping import de.gematik.ti.erp.app.prescription.repository.normSizeMapping import de.gematik.ti.erp.app.substitutionAllowed import de.gematik.ti.erp.app.supplyForm import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center import de.gematik.ti.erp.app.utils.extensions.dateTimeMediumText import de.gematik.ti.erp.app.utils.extensions.temporalText import de.gematik.ti.erp.app.utils.isNotNullOrEmpty import de.gematik.ti.erp.app.utils.letNotNullOnCondition -import io.github.aakira.napier.Napier import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone +// @TODO: Implement UIStateMachine class PrescriptionDetailMedicationScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry @@ -79,43 +85,65 @@ class PrescriptionDetailMedicationScreen( @Composable override fun Content() { val taskId = - remember { requireNotNull(navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId)) } + remember { + requireNotNull( + navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID) + ) + } val selectedMedication = remember { requireNotNull( navBackStackEntry.arguments?.getString( - PrescriptionDetailRoutes.SelectedMedication + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_SELECTED_MEDICATION ) ) } - Napier.i { "selectedMedication" } - Napier.i { selectedMedication } - val prescriptionDataMedication = remember(selectedMedication) { - fromNavigationString(selectedMedication) - } + + val prescriptionDataMedication = + remember(selectedMedication) { + fromNavigationString(selectedMedication) + } val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced - val scaffoldState = rememberScaffoldState() - val listState = rememberLazyListState() - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.Screen), - scaffoldState = scaffoldState, - listState = listState, - onBack = navController::popBackStack, - topBarTitle = stringResource(R.string.synced_medication_detail_header), - navigationMode = NavigationBarMode.Back, - snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, - actions = {} - ) { innerPadding -> - PrescriptionDetailMedicationScreenContent( - listState, - innerPadding, - navController, - prescriptionDataMedication, - syncedPrescription - ) - } + + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val scaffoldState = rememberScaffoldState() + val listState = rememberLazyListState() + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.Screen), + scaffoldState = scaffoldState, + listState = listState, + onBack = navController::popBackStack, + topBarTitle = stringResource(R.string.synced_medication_detail_header), + navigationMode = NavigationBarMode.Back, + snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, + actions = {} + ) { innerPadding -> + PrescriptionDetailMedicationScreenContent( + listState, + innerPadding, + navController, + prescriptionDataMedication, + syncedPrescription + ) + } + } + ) } } @@ -127,14 +155,16 @@ private fun PrescriptionDetailMedicationScreenContent( prescriptionDataMedication: PrescriptionData.Medication?, syncedPrescription: PrescriptionData.Synced? ) { - val medication = when (prescriptionDataMedication) { - is PrescriptionData.Medication.Dispense -> prescriptionDataMedication.medicationDispense.medication - is PrescriptionData.Medication.Request -> prescriptionDataMedication.medicationRequest.medication - null -> null - } + val medication = + when (prescriptionDataMedication) { + is PrescriptionData.Medication.Dispense -> prescriptionDataMedication.medicationDispense.medication + is PrescriptionData.Medication.Request -> prescriptionDataMedication.medicationRequest.medication + null -> null + } LazyColumn( state = listState, - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(innerPadding) .testTag(TestTag.Prescriptions.Details.Medication.Content), @@ -143,28 +173,18 @@ private fun PrescriptionDetailMedicationScreenContent( item { SpacerMedium() } - when (medication) { - is SyncedTaskData.MedicationPZN -> pznMedicationInformation(medication) - is SyncedTaskData.MedicationIngredient -> ingredientMedicationInformation(medication) { - ingredient -> - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailIngredientsScreen.path( - selectedIngredient = ingredient.toNavigationString() - ) - ) - } - - is SyncedTaskData.MedicationCompounding -> compoundingMedicationInformation(medication) { - ingredient -> - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailIngredientsScreen.path( - selectedIngredient = ingredient.toNavigationString() + medication?.let { med -> + medicationInformation( + med, + onClickIngredient = { + ingredient -> + navController.navigate( + PrescriptionDetailRoutes.PrescriptionDetailIngredientsScreen.path( + selectedIngredient = ingredient.toNavigationString() + ) ) - ) - } - - is SyncedTaskData.MedicationFreeText -> freeTextMedicationInformation(medication) - null -> {} + } + ) } syncedPrescription?.authoredOn?.let { prescriptionInformation(it) } @@ -221,7 +241,12 @@ private fun LazyListScope.medicationDispense(medicationDispense: SyncedTaskData. } } -private fun LazyListScope.pznMedicationInformation(medication: SyncedTaskData.MedicationPZN) { +private fun LazyListScope.medicationInformation( + medication: SyncedTaskData.Medication, + // onClickMedication: (SyncedTaskData.Medication) -> Unit, + // Todo: List contained Medications and navigate to this screen with contained medication + onClickIngredient: (SyncedTaskData.Ingredient) -> Unit +) { item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.Name), @@ -232,7 +257,10 @@ private fun LazyListScope.pznMedicationInformation(medication: SyncedTaskData.Me letNotNullOnCondition( first = medication.amount, condition = { - medication.amount?.numerator?.value?.isNotNullOrEmpty() == true + medication.amount + ?.numerator + ?.value + ?.isNotNullOrEmpty() == true }, transform = { item { @@ -240,50 +268,9 @@ private fun LazyListScope.pznMedicationInformation(medication: SyncedTaskData.Me } } ) - medication.normSizeCode?.let { - item { - NormSizeLabel(it) - } - } - item { - PZNLabel(medication.uniqueIdentifier) - } - medication.form?.let { - item { - FormLabel(it) - } - } - - if (medication.category != SyncedTaskData.MedicationCategory.UNKNOWN) { - item { - CategoryLabel(medication.category) - } - } - - item { - VaccineLabel(medication.vaccine) - } - medication.lotNumber?.let { - item { - LotNumberLabel(it) - } - } - medication.expirationDate?.let { - item { - ExpirationDateLabel(it) - } - } -} - -private fun LazyListScope.ingredientMedicationInformation( - medication: SyncedTaskData.MedicationIngredient, - onClickIngredient: (SyncedTaskData.Ingredient) -> Unit -) { - medication.ingredients.forEach { ingredient -> + medication.packaging?.let { item { - IngredientNameLabel(ingredient.text) { - onClickIngredient(ingredient) - } + PackagingLabel(it) } } medication.normSizeCode?.let { @@ -291,65 +278,18 @@ private fun LazyListScope.ingredientMedicationInformation( NormSizeLabel(it) } } - - medication.form?.let { - item { - FormLabel(it) - } - } - letNotNullOnCondition( - first = medication.amount, - condition = { - medication.amount?.numerator?.value?.isNotNullOrEmpty() == true - }, - transform = { - item { - AmountLabel(it) - } - } - ) - item { - CategoryLabel(medication.category) - } item { - VaccineLabel(medication.vaccine) - } - medication.lotNumber?.let { - item { - LotNumberLabel(it) + medication.identifier.pzn?.let { + IdentifierLabel(identifier = it, label = stringResource(id = R.string.pres_detail_medication_label_pzn)) } - } - medication.expirationDate?.let { - item { - ExpirationDateLabel(it) + medication.identifier.ask?.let { + IdentifierLabel(identifier = it, label = stringResource(R.string.mediction_detiail_ask)) } - } -} - -private fun LazyListScope.compoundingMedicationInformation( - medication: SyncedTaskData.MedicationCompounding, - onClickIngredient: (SyncedTaskData.Ingredient) -> Unit -) { - item { - Label( - text = medication.name(), - label = stringResource(R.string.medication_compounding_name) - ) - } - letNotNullOnCondition( - first = medication.amount, - condition = { - medication.amount?.numerator?.value?.isNotNullOrEmpty() == true - }, - transform = { - item { - AmountLabel(it) - } + medication.identifier.atc?.let { + IdentifierLabel(identifier = it, label = stringResource(R.string.mediction_detiail_atc)) } - ) - medication.packaging?.let { - item { - PackagingLabel(it) + medication.identifier.snomed?.let { + IdentifierLabel(identifier = it, label = stringResource(R.string.mediction_detiail_snomed)) } } medication.form?.let { @@ -387,38 +327,12 @@ private fun LazyListScope.compoundingMedicationInformation( } } -private fun LazyListScope.freeTextMedicationInformation(medication: SyncedTaskData.MedicationFreeText) { - item { - Label(text = medication.name(), label = stringResource(R.string.medication_freetext_name)) - } - item { - CategoryLabel(medication.category) - } - item { - VaccineLabel(medication.vaccine) - } - medication.form?.let { - item { - FormLabel(it) - } - } - medication.lotNumber?.let { - item { - LotNumberLabel(it) - } - } - medication.expirationDate?.let { - item { - ExpirationDateLabel(it) - } - } -} - @Composable private fun NormSizeLabel(normSizeCode: String) { - val description = normSizeMapping[normSizeCode]?.let { resourceId -> - stringResource(resourceId) - } + val description = + normSizeMapping[normSizeCode]?.let { resourceId -> + stringResource(resourceId) + } Label( text = "$normSizeCode${description?.let { " - $it" } ?: ""}", label = stringResource(id = R.string.pres_detail_medication_label_normsize) @@ -426,23 +340,25 @@ private fun NormSizeLabel(normSizeCode: String) { } @Composable -private fun PZNLabel(uniqueIdentifier: String) { +private fun IdentifierLabel(identifier: String, label: String) { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.PZN), - text = uniqueIdentifier, - label = stringResource(id = R.string.pres_detail_medication_label_pzn) + text = identifier, + label = label ) } @Composable fun FormLabel(form: String) { Label( - modifier = Modifier + modifier = + Modifier .testTag(TestTag.Prescriptions.Details.Medication.SupplyForm) .semantics { supplyForm = form }, - text = codeToFormMapping[form]?.let { resourceId -> + text = + codeToFormMapping[form]?.let { resourceId -> stringResource(resourceId) } ?: form, label = stringResource(id = R.string.pres_detail_medication_label_dosage_form) @@ -452,11 +368,12 @@ fun FormLabel(form: String) { @Composable private fun DosageInstructionLabel(dosageInstruction: String?) { dosageInstruction?.let { instruction -> - val text = if (instruction.lowercase() == "dj") { - stringResource(R.string.pres_detail_medication_dj) - } else { - instruction - } + val text = + if (instruction.lowercase() == "dj") { + stringResource(R.string.pres_detail_medication_dj) + } else { + instruction + } Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.DosageInstruction), @@ -479,26 +396,29 @@ private fun QuantityLabel(quantity: Int) { @Composable private fun CategoryLabel(category: SyncedTaskData.MedicationCategory) { - val text = when (category) { - SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> stringResource(R.string.medicines_bandages) - SyncedTaskData.MedicationCategory.BTM -> stringResource(R.string.narcotics) - SyncedTaskData.MedicationCategory.AMVV -> stringResource(R.string.amvv) - else -> null - } + val text = + when (category) { + SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> stringResource(R.string.medicines_bandages) + SyncedTaskData.MedicationCategory.BTM -> stringResource(R.string.narcotics) + SyncedTaskData.MedicationCategory.AMVV -> stringResource(R.string.amvv) + else -> null + } Label( - modifier = Modifier + modifier = + Modifier .testTag(TestTag.Prescriptions.Details.Medication.Category) .then( if (BuildKonfig.INTERNAL) { Modifier.semantics { - medicationCategory = when (category) { - SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> "00" - SyncedTaskData.MedicationCategory.BTM -> "01" - SyncedTaskData.MedicationCategory.AMVV -> "02" - SyncedTaskData.MedicationCategory.SONSTIGES -> "03" - else -> null - } + medicationCategory = + when (category) { + SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> "00" + SyncedTaskData.MedicationCategory.BTM -> "01" + SyncedTaskData.MedicationCategory.AMVV -> "02" + SyncedTaskData.MedicationCategory.SONSTIGES -> "03" + else -> null + } } } else { Modifier @@ -510,7 +430,7 @@ private fun CategoryLabel(category: SyncedTaskData.MedicationCategory) { } @Composable -private fun AmountLabel(amount: SyncedTaskData.Ratio) { +private fun AmountLabel(amount: Ratio) { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.Amount), text = "${amount.numerator?.value} ${amount.numerator?.unit}", @@ -520,11 +440,12 @@ private fun AmountLabel(amount: SyncedTaskData.Ratio) { @Composable private fun BvgLabel(bvg: Boolean) { - val text = if (bvg) { - stringResource(id = R.string.pres_detail_yes) - } else { - stringResource(id = R.string.pres_detail_no) - } + val text = + if (bvg) { + stringResource(id = R.string.pres_detail_yes) + } else { + stringResource(id = R.string.pres_detail_no) + } Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.BVG), text = text, @@ -550,13 +471,15 @@ private fun NoteLabel(note: String) { @Composable private fun SubstitutionLabel(substitutionInfo: Boolean) { - val text = if (substitutionInfo) { - stringResource(id = R.string.pres_detail_yes) - } else { - stringResource(id = R.string.pres_detail_no) - } + val text = + if (substitutionInfo) { + stringResource(id = R.string.pres_detail_yes) + } else { + stringResource(id = R.string.pres_detail_no) + } Label( - modifier = Modifier + modifier = + Modifier .testTag(TestTag.Prescriptions.Details.Medication.SubstitutionAllowed) .semantics { substitutionAllowed = substitutionInfo @@ -610,11 +533,12 @@ private fun ExpirationDateLabel(expirationDate: FhirTemporal) { @Composable private fun VaccineLabel(isVaccine: Boolean) { - val text = if (isVaccine) { - stringResource(id = R.string.pres_detail_yes) - } else { - stringResource(id = R.string.pres_detail_no) - } + val text = + if (isVaccine) { + stringResource(id = R.string.pres_detail_yes) + } else { + stringResource(id = R.string.pres_detail_no) + } Label( text = text, label = stringResource(id = R.string.pres_detail_medication_vaccine) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailOrganizationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailOrganizationScreen.kt index d2c597de..4c2855f0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailOrganizationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailOrganizationScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -29,12 +29,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag @@ -42,12 +44,15 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center class PrescriptionDetailOrganizationScreen( override val navController: NavController, @@ -56,28 +61,45 @@ class PrescriptionDetailOrganizationScreen( @Composable override fun Content() { val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID ) - } + } ?: "" val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced - val organization = syncedPrescription?.organization - val listState = rememberLazyListState() - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.Screen), - topBarTitle = stringResource(R.string.pres_detail_organization_header), - listState = listState, - onBack = navController::popBackStack, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - PrescriptionDetailOrganizationScreenContent( - listState, - innerPadding, - organization - ) - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val organization = syncedPrescription?.organization + val listState = rememberLazyListState() + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.Screen), + topBarTitle = stringResource(R.string.pres_detail_organization_header), + listState = listState, + onBack = navController::popBackStack, + navigationMode = NavigationBarMode.Back + ) { innerPadding -> + PrescriptionDetailOrganizationScreenContent( + listState, + innerPadding, + organization + ) + } + } + ) } } @@ -89,7 +111,8 @@ private fun PrescriptionDetailOrganizationScreenContent( ) { val noValueText = stringResource(R.string.pres_details_no_value) LazyColumn( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(innerPadding) .testTag(TestTag.Prescriptions.Details.Organization.Content), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPatientScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPatientScreen.kt index 1f2d8e83..a0f86c53 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPatientScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPatientScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag @@ -45,13 +47,16 @@ import de.gematik.ti.erp.app.insuranceState import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.repository.statusMapping +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center import de.gematik.ti.erp.app.utils.extensions.temporalText import kotlinx.datetime.TimeZone @@ -62,30 +67,47 @@ class PrescriptionDetailPatientScreen( @Composable override fun Content() { val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID ) - } + } ?: "" val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced - val patient = syncedPrescription?.patient - val insurance = syncedPrescription?.insurance - val listState = rememberLazyListState() - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.Screen), - topBarTitle = stringResource(R.string.pres_detail_patient_header), - listState = listState, - onBack = navController::popBackStack, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - PrescriptionDetailPatientScreenContent( - listState, - innerPadding, - patient, - insurance - ) - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val patient = syncedPrescription?.patient + val insurance = syncedPrescription?.insurance + val listState = rememberLazyListState() + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.Screen), + topBarTitle = stringResource(R.string.pres_detail_patient_header), + listState = listState, + onBack = navController::popBackStack, + navigationMode = NavigationBarMode.Back + ) { innerPadding -> + PrescriptionDetailPatientScreenContent( + listState, + innerPadding, + patient, + insurance + ) + } + } + ) } } @@ -98,7 +120,8 @@ private fun PrescriptionDetailPatientScreenContent( ) { val noValueText = stringResource(R.string.pres_details_no_value) LazyColumn( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(innerPadding) .testTag(TestTag.Prescriptions.Details.Patient.Content), @@ -130,7 +153,8 @@ private fun PrescriptionDetailPatientScreenContent( item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.BirthDate), - text = remember(LocalConfiguration.current, patient) { + text = + remember(LocalConfiguration.current, patient) { patient?.birthdate?.let { temporalText(it, TimeZone.currentSystemDefault()) } ?: noValueText @@ -147,7 +171,8 @@ private fun PrescriptionDetailPatientScreenContent( } item { Label( - modifier = Modifier + modifier = + Modifier .testTag(TestTag.Prescriptions.Details.Patient.InsuranceState) .semantics { insuranceState = insurance?.status diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPrescriberScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPrescriberScreen.kt index 59fe50f2..4ec78193 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPrescriberScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailPrescriberScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -29,23 +29,28 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center class PrescriptionDetailPrescriberScreen( override val navController: NavController, @@ -54,27 +59,44 @@ class PrescriptionDetailPrescriberScreen( @Composable override fun Content() { val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID ) - } + } ?: "" val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced - val practitioner = syncedPrescription?.practitioner - val listState = rememberLazyListState() - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.pres_detail_practitioner_header), - listState = listState, - onBack = navController::popBackStack, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - PrescriptionDetailPrescriberScreenContent( - listState, - innerPadding, - practitioner - ) - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val syncedPrescription = prescription as? PrescriptionData.Synced + val practitioner = syncedPrescription?.practitioner + val listState = rememberLazyListState() + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.pres_detail_practitioner_header), + listState = listState, + onBack = navController::popBackStack, + navigationMode = NavigationBarMode.Back + ) { innerPadding -> + PrescriptionDetailPrescriberScreenContent( + listState, + innerPadding, + practitioner + ) + } + } + ) } } @@ -86,7 +108,8 @@ private fun PrescriptionDetailPrescriberScreenContent( ) { val noValueText = stringResource(R.string.pres_details_no_value) LazyColumn( - modifier = Modifier + modifier = + Modifier .fillMaxSize() .padding(innerPadding), state = listState, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt index 7824ff5a..b287b33c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt @@ -1,130 +1,275 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:OptIn(ExperimentalMaterialApi::class) - package de.gematik.ti.erp.app.prescription.detail.ui -import android.net.Uri -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.SnackbarResult import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackPrescriptionDetailPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.ConsentState.Companion.isConsentGranted +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.navigation.toNavigationString +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.presentation.model.InvoiceCardUiState +import de.gematik.ti.erp.app.pkv.presentation.rememberConsentController +import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.pkv.ui.screens.HandleConsentState import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController +import de.gematik.ti.erp.app.prescription.detail.presentation.rememberSharePrescriptionController +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailBottomSheetNavigationData +import de.gematik.ti.erp.app.prescription.detail.ui.preview.PrescriptionDetailPreview +import de.gematik.ti.erp.app.prescription.detail.ui.preview.PrescriptionDetailPreviewParameter +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.provideEmailIntent +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold import kotlinx.coroutines.launch -const val MissingValue = "---" +const val MISSING_VALUE = "---" -@OptIn(ExperimentalMaterialApi::class) class PrescriptionDetailScreen( override val navController: NavController, override val navBackStackEntry: NavBackStackEntry ) : Screen() { + @Suppress("CyclomaticComplexMethod") @Composable override fun Content() { - val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) - ) - } + val taskId = + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID + ) ?: return + + val snackbar = LocalSnackbarScaffold.current + val dialog = LocalDialog.current + val context = LocalContext.current + val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescription by prescriptionDetailsController.prescriptionState - val activeProfile by prescriptionDetailsController.activeProfileState - val scaffoldState = rememberScaffoldState() - val listState = rememberLazyListState() - val sheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden, - confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded } - ) - val coroutineScope = rememberCoroutineScope() - var infoBottomSheetContent: PrescriptionDetailBottomSheetContent? by remember { mutableStateOf(null) } - - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - infoBottomSheetContent?.let { analytics.trackPrescriptionDetailPopUps(it) } - } else { - analytics.onPopUpClosed() - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - LaunchedEffect(infoBottomSheetContent) { - if (infoBottomSheetContent != null) { - sheetState.show() - } else { - sheetState.hide() - } - } - ModalBottomSheetLayout( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.Screen), - sheetState = sheetState, - sheetContent = { - Box( - Modifier - .heightIn(min = 56.dp) - .navigationBarsPadding() - ) { - infoBottomSheetContent?.let { - PrescriptionDetailInfoSheetContent(infoContent = it) - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + val isMedicationPlanEnabled by prescriptionDetailsController.isMedicationPlanEnabled.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() } }, - sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) - ) { - activeProfile?.let { + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (profile, prescription) -> + // TODO: Too many controllers in one screen, consider refactoring + val consentController = rememberConsentController() + val invoicesController = rememberInvoiceController(profileId = profile.id) + + val activeProfileIsPKVProfile = + profile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV + + val invoice by produceState(null) { + invoicesController.getInvoiceForTaskId(prescription.taskId).collect { + value = it + } + } + + val consentState by consentController.consentState.collectAsStateWithLifecycle() + val consentGranted = remember(consentState) { consentState.isConsentGranted() } + val deletePrescriptionState by + prescriptionDetailsController.prescriptionDeleted.collectAsStateWithLifecycle() + val onClickDeletePrescriptionEvent = ComposableEvent() + + val shareHandler = rememberSharePrescriptionController(profile.id) + + val mailAddress = stringResource(R.string.settings_contact_mail_address) + val subject = stringResource(R.string.settings_feedback_mail_subject) + + val scope = rememberCoroutineScope() + + val ssoTokenValid = + remember(profile.ssoTokenScope) { + profile.isSSOTokenValid() + } + + val invoiceState = invoicesController.uiState(consentState, ssoTokenValid, invoice) + + LaunchedEffect(Unit) { + if (activeProfileIsPKVProfile && + ssoTokenValid && + consentState == ConsentState.ValidState.UnknownConsent + ) { + consentController.getChargeConsent(profile.id) + } + } + + HandleDeletePrescriptionState( + state = deletePrescriptionState, + dialog = dialog, + onShowCardWall = { + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(profile.id)) + }, + onConfirmDialogRequest = { + (sendFeedBackMessage, errorMessage): Pair, + deletePrescriptionLocally: Boolean + -> + if (deletePrescriptionLocally) { + prescriptionDetailsController.deletePrescriptionFromLocal(profile.id, taskId) + navController.popBackStack( + PrescriptionDetailRoutes.PrescriptionDetailScreen.route, + true + ) + } + if (sendFeedBackMessage) { + context.handleIntent( + provideEmailIntent( + address = mailAddress, + body = errorMessage, + subject = subject + ) + ) + } + }, + onRetry = { + prescriptionDetailsController.deletePrescription(profile.id, taskId) + }, + onDismiss = { + prescriptionDetailsController.resetDeletePrescriptionState() + }, + onBack = { + navController.popBackStack( + PrescriptionDetailRoutes.PrescriptionDetailScreen.route, + true + ) + } + ) + + LaunchedEffect(Unit) { + if (ssoTokenValid && consentState == ConsentState.ValidState.UnknownConsent && !consentGranted) { + consentController.getChargeConsent(profile.id) + } + } + + if (activeProfileIsPKVProfile) { + val actionString = stringResource(R.string.consent_action_to_invoices) + val consentRevokedInfo = stringResource(R.string.consent_revoked_info) + val consentGrantedInfo = stringResource(R.string.consent_granted_info) + + HandleConsentState( + consentState = consentState, + dialog = dialog, + onShowCardWall = { + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(profile.id)) + }, + onRetry = { consentContext -> + when (consentContext) { + ConsentContext.GetConsent -> consentController.getChargeConsent(profile.id) + ConsentContext.GrantConsent -> consentController.grantChargeConsent(profile.id) + ConsentContext.RevokeConsent -> {} // revoke is not available on prescription details + } + }, + onConsentGranted = { + scope.launch { + val result = + snackbar.showSnackbar( + message = consentGrantedInfo, + actionLabel = actionString + ) + when (result) { + SnackbarResult.Dismissed -> {} + SnackbarResult.ActionPerformed -> + navController.navigate(PkvRoutes.InvoiceListScreen.path(profile.id)) + } + } + }, + onConsentRevoked = { + scope.launch { + val result = + snackbar.showSnackbar( + message = consentRevokedInfo, + actionLabel = actionString + ) + when (result) { + SnackbarResult.Dismissed -> {} + SnackbarResult.ActionPerformed -> + navController.navigate(PkvRoutes.InvoiceListScreen.path(profile.id)) + } + } + } + ) + } + + val scaffoldState = rememberScaffoldState() + val listState = rememberLazyListState() + val medicationSchedule by prescriptionDetailsController.medicationSchedule.collectAsStateWithLifecycle() + + DeletePrescriptionDialog( + dialog = dialog, + event = onClickDeletePrescriptionEvent, + isPKVProfile = activeProfileIsPKVProfile, + isPrescriptionRedeemed = prescription.redeemedOn != null + ) { + prescriptionDetailsController.deletePrescription( + profile.id, + prescription.taskId + ) + } PrescriptionDetailScreenScaffold( - activeProfile = it, + activeProfile = profile, prescription = prescription, + medicationSchedule = medicationSchedule, + invoiceCardState = invoiceState, scaffoldState = scaffoldState, listState = listState, - // TODO: Hoist it out - prescriptionDetailController = prescriptionDetailsController, - // TODO: Hoist it out - navController = navController, + isMedicationPlanEnabled = isMedicationPlanEnabled, onClickMedication = { medication -> navController.navigate( PrescriptionDetailRoutes.PrescriptionDetailMedicationScreen.path( @@ -133,18 +278,239 @@ class PrescriptionDetailScreen( ) ) }, - onChangeSheetContent = { - infoBottomSheetContent = it - coroutineScope.launch { - sheetState.show() - } - }, onGrantConsent = { - prescriptionDetailsController.grantConsent() + consentController.grantChargeConsent(profile.id) + }, + onClickTechnicalInformation = { + navController.navigate( + PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.path( + taskId = prescription.taskId + ) + ) + }, + onChangePrescriptionName = { newName -> + prescriptionDetailsController.updateScannedTaskName(prescription.taskId, newName) + }, + onSwitchRedeemed = { redeem -> + prescriptionDetailsController.redeemScannedTask(prescription.taskId, redeem) + }, + onClickDeletePrescription = { + onClickDeletePrescriptionEvent.trigger(Unit) + }, + onSharePrescription = { + shareHandler.share(taskId = prescription.taskId, prescription.accessCode) + }, + onClickRedeemLocal = { + navController.navigate( + RedeemRoutes.RedeemLocal.path( + taskId = prescription.taskId + ) + ) + }, + onClickRedeemOnline = { + navController.navigate( + PharmacyRoutes.PharmacyStartScreenModal.path(taskId = prescription.taskId) + ) + }, + onClickMedicationPlan = { + navController.navigate( + MedicationPlanRoutes.MedicationPlanPerPrescription.path( + taskId = taskId + ) + ) + }, + onNavigateToRoute = { route -> + navController.navigate(route) + }, + onShowHowLongValidBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.HowLongValidBottomSheetScreen.path( + taskId = prescription.taskId + ) + ) + }, + onShowInfoBottomSheet = + PrescriptionDetailBottomSheetNavigationData( + selPayerPrescriptionBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.SelPayerPrescriptionBottomSheetScreen.path( + titleId = R.string.pres_details_exp_sel_payer_prescription, + infoId = R.string.pres_details_exp_sel_payer_info + ) + ) + }, + additionalFeeNotExemptBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.AdditionalFeeNotExemptBottomSheetScreen.path( + titleId = R.string.pres_details_exp_add_fee_title, + infoId = R.string.pres_details_exp_add_fee_info + ) + ) + }, + additionalFeeExemptBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.AdditionalFeeExemptBottomSheetScreen.path( + titleId = R.string.pres_details_exp_no_add_fee_title, + infoId = R.string.pres_details_exp_no_add_fee_info + ) + ) + }, + failureBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.FailureBottomSheetScreen.path( + titleId = R.string.pres_details_exp_failure_title, + infoId = R.string.pres_details_exp_failure_info + ) + ) + }, + scannedPrescriptionBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.ScannedBottomSheetScreen.path( + titleId = R.string.pres_details_exp_scanned_title, + infoId = R.string.pres_details_exp_scanned_info + ) + ) + }, + directAssignmentBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.DirectAssignmentBottomSheetScreen.path( + titleId = R.string.pres_details_exp_da_title, + infoId = R.string.pres_details_exp_da_info + ) + ) + }, + substitutionAllowedBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.SubstitutionAllowedBottomSheetScreen.path( + titleId = R.string.prescription_details_substitution_allowed, + infoId = R.string.prescription_details_substitution_allowed_info + ) + ) + }, + substitutionNotAllowedBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.SubstitutionNotAllowedBottomSheetScreen.path( + titleId = R.string.prescription_details_substitution_not_allowed, + infoId = R.string.prescription_details_substitution_not_allowed_info + ) + ) + }, + emergencyFeeExemptBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.EmergencyFeeExemptBottomSheetScreen.path( + titleId = R.string.pres_details_exp_em_fee_title, + infoId = R.string.pres_details_exp_em_fee_info + ) + ) + }, + emergencyFeeNotExemptBottomSheet = { + navController.navigate( + PrescriptionDetailRoutes.EmergencyFeeNotExemptBottomSheetScreen.path( + titleId = R.string.pres_details_exp_no_em_fee_title, + infoId = R.string.pres_details_exp_no_em_fee_info + ) + ) + } + ), + onClickInvoice = { + navController.navigate( + PkvRoutes.InvoiceDetailsScreen.path( + profileId = profile.id, + taskId = prescription.taskId + ) + ) }, onBack = navController::popBackStack ) } + ) + } +} + +@Composable +private fun DeletePrescriptionDialog( + dialog: DialogScaffold, + event: ComposableEvent, + isPrescriptionRedeemed: Boolean = false, + isPKVProfile: Boolean = false, + onConfirmRequest: () -> Unit +) { + val title = if (isPKVProfile && isPrescriptionRedeemed) { + stringResource(R.string.pres_detail_delete_prescription_and_recipe) + } else { + stringResource(R.string.pres_detail_delete_prescription) + } + val info = if (isPKVProfile) { + stringResource(R.string.pres_detail_delete_and_recipe_msg) + } else { + stringResource(R.string.pres_detail_delete_msg) + } + event.listen { + dialog.show { dialog -> + ErezeptAlertDialog( + title = title, + bodyText = info, + confirmText = stringResource(R.string.pres_detail_delete_yes), + dismissText = stringResource(R.string.pres_detail_delete_no), + onDismissRequest = { + dialog.dismiss() + }, + onConfirmRequest = { + onConfirmRequest() + dialog.dismiss() + } + ) } } } + +@LightDarkPreview +@Composable +fun PrescriptionDetailScreenPreview( + @PreviewParameter(PrescriptionDetailPreviewParameter::class) previewData: PrescriptionDetailPreview +) { + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + PreviewAppTheme { + PrescriptionDetailScreenScaffold( + activeProfile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insurantName = "Max Mustermann", + insuranceIdentifier = "1234567890", + insuranceName = "Muster AG", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SUN_DEW, + avatar = ProfilesData.Avatar.Baby, + image = null, + lastAuthenticated = null, + ssoTokenScope = null + ), + scaffoldState = scaffoldState, + listState = listState, + prescription = previewData.prescription, + medicationSchedule = null, + invoiceCardState = InvoiceCardUiState.NoInvoice, + onShowInfoBottomSheet = PrescriptionDetailBottomSheetNavigationData(), + now = previewData.now, + onSwitchRedeemed = {}, + onNavigateToRoute = {}, + onClickMedication = {}, + onChangePrescriptionName = {}, + onGrantConsent = {}, + onClickRedeemLocal = {}, + onClickRedeemOnline = {}, + onClickTechnicalInformation = {}, + onClickDeletePrescription = {}, + onClickMedicationPlan = {}, + onSharePrescription = {}, + onShowHowLongValidBottomSheet = {}, + onClickInvoice = {}, + onBack = {}, + isMedicationPlanEnabled = false + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenScaffold.kt index e81226e6..5dde5efe 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenScaffold.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui -import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.DropdownMenu @@ -32,79 +31,57 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Share import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pkv.ui.CheckConsentState -import de.gematik.ti.erp.app.pkv.ui.GrantConsentDialog -import de.gematik.ti.erp.app.pkv.ui.onGrantConsent -import de.gematik.ti.erp.app.pkv.ui.rememberDeprecatedConsentController -import de.gematik.ti.erp.app.prescription.detail.presentation.PrescriptionDetailController -import de.gematik.ti.erp.app.prescription.detail.presentation.rememberSharePrescriptionController -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.pkv.presentation.model.InvoiceCardUiState +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailBottomSheetNavigationData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.PrescriptionType import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import kotlinx.coroutines.launch +import de.gematik.ti.erp.app.utils.letNotNull +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +@Suppress("LongParameterList", "FunctionNaming") @Composable fun PrescriptionDetailScreenScaffold( activeProfile: ProfilesUseCaseData.Profile, scaffoldState: ScaffoldState, listState: LazyListState, - prescriptionDetailController: PrescriptionDetailController, prescription: PrescriptionData.Prescription?, - navController: NavController, + medicationSchedule: MedicationSchedule?, + invoiceCardState: InvoiceCardUiState, + onShowInfoBottomSheet: PrescriptionDetailBottomSheetNavigationData, + now: Instant = Clock.System.now(), + isMedicationPlanEnabled: Boolean, + onSwitchRedeemed: (Boolean) -> Unit, + onNavigateToRoute: (String) -> Unit, onClickMedication: (PrescriptionData.Medication) -> Unit, - onChangeSheetContent: (PrescriptionDetailBottomSheetContent?) -> Unit, + onChangePrescriptionName: (String) -> Unit, onGrantConsent: () -> Unit, + onClickRedeemLocal: () -> Unit, + onClickRedeemOnline: () -> Unit, + onClickTechnicalInformation: () -> Unit, + onClickDeletePrescription: () -> Unit, + onClickMedicationPlan: (PrescriptionType) -> Unit, + onSharePrescription: () -> Unit, + onShowHowLongValidBottomSheet: () -> Unit, + onClickInvoice: () -> Unit, onBack: () -> Unit ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val ssoTokenValid = rememberSaveable(activeProfile.ssoTokenScope) { - activeProfile.isSSOTokenValid() - } - // TODO: (Fail on design pattern) Need to add this to the controller in the screen level - val consentController = rememberDeprecatedConsentController(profile = activeProfile) - val shareHandler = rememberSharePrescriptionController(activeProfile.id) - - var consentGranted: Boolean? by remember { mutableStateOf(null) } - - var showGrantConsentDialog by remember { mutableStateOf(false) } - - // TODO: Move check to viewmodel - CheckConsentState(consentController, ssoTokenValid, scaffoldState, context) { - consentGranted = it - } - if (showGrantConsentDialog && activeProfile.isSSOTokenValid()) { - GrantConsentDialog( - onCancel = onBack - ) { - onGrantConsent(context, scope, consentController, scaffoldState) { - onGrantConsent() - consentGranted = it - showGrantConsentDialog = false - } - } - } AnimatedElevationScaffold( scaffoldState = scaffoldState, listState = listState, @@ -113,77 +90,64 @@ fun PrescriptionDetailScreenScaffold( navigationMode = NavigationBarMode.Close, snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, actions = { - val authenticator = LocalAuthenticator.current - val deletePrescriptionsHandle = remember { - DeletePrescriptions( - prescriptionDetailsController = prescriptionDetailController, - authenticator = authenticator - ) - } - prescription?.let { - prescription.accessCode?.let { accessCode -> + letNotNull( + prescription, + prescription?.taskId, + prescription?.accessCode + ) { actualPrescription, taskId, accessCode -> + if (accessCode.isNotEmpty()) { // not for direct assignment IconButton(onClick = { - shareHandler.share(taskId = prescription.taskId, accessCode) + onSharePrescription() }) { Icon(Icons.Rounded.Share, null, tint = AppTheme.colors.primary700) } } - DeleteAction(it) { - // TODO: This needs to be done in the PrescriptionDetailsScreen, - // please do stateHoisting so that it is not hidden inside, becase this also has a onBack. - // It should be something like maybeOnBack with the deleteState sent with it and then in the - // screen we decide what to do - - val deleteState = deletePrescriptionsHandle.deletePrescription( - profileId = prescription.profileId, - taskId = prescription.taskId - ) - when (deleteState) { - is PrescriptionServiceErrorState -> { - deleteErrorMessage(context, deleteState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - is DeletePrescriptions.State.Deleted -> onBack() - } - } + PrescriptionDetailsDropdownMenu( + isDeletable = (actualPrescription as? PrescriptionData.Synced)?.isDeletable ?: true, + onClickDelete = onClickDeletePrescription + ) } } ) { when (prescription) { is PrescriptionData.Synced -> SyncedPrescriptionOverview( - navController = navController, - consentGranted = consentGranted, - ssoTokenValid = ssoTokenValid, - onGrantConsent = { showGrantConsentDialog = true }, + invoiceCardState = invoiceCardState, + onGrantConsent = onGrantConsent, activeProfile = activeProfile, listState = listState, prescription = prescription, + now = now, + isMedicationPlanEnabled = isMedicationPlanEnabled, + onClickInvoice = onClickInvoice, + medicationSchedule = medicationSchedule, onClickMedication = onClickMedication, - onShowInfo = { - onChangeSheetContent(it) - } + onNavigateToRoute = onNavigateToRoute, + onClickRedeemLocal = onClickRedeemLocal, + onClickRedeemOnline = onClickRedeemOnline, + onShowInfoBottomSheet = onShowInfoBottomSheet, + onShowHowLongValidBottomSheet = onShowHowLongValidBottomSheet, + onClickMedicationPlan = { onClickMedicationPlan(PrescriptionType.SyncedTask) } ) + is PrescriptionData.Scanned -> ScannedPrescriptionOverview( - navController = navController, listState = listState, prescription = prescription, + medicationSchedule = medicationSchedule, + isMedicationPlanEnabled = isMedicationPlanEnabled, onSwitchRedeemed = { - prescriptionDetailController.redeemScannedTask( - taskId = prescription.taskId, - redeem = it - ) - }, - onShowInfo = { - onChangeSheetContent(it) + onSwitchRedeemed(it) }, - onChangePrescriptionName = { newName -> - prescriptionDetailController.updateScannedTaskName(prescription.taskId, newName) - } + onClickRedeemLocal = onClickRedeemLocal, + onClickRedeemOnline = onClickRedeemOnline, + onChangePrescriptionName = onChangePrescriptionName, + onClickTechnicalInformation = onClickTechnicalInformation, + onClickMedicationPlan = { onClickMedicationPlan(PrescriptionType.ScannedTask) }, + onShowScannedPrescriptionBottomSheet = onShowInfoBottomSheet.scannedPrescriptionBottomSheet ) + else -> { // do nothing } @@ -192,24 +156,12 @@ fun PrescriptionDetailScreenScaffold( } @Composable -private fun DeleteAction( - prescription: PrescriptionData.Prescription, - onClickDelete: suspend () -> Unit +private fun PrescriptionDetailsDropdownMenu( + isDeletable: Boolean, + onClickDelete: () -> Unit ) { - var showDeletePrescriptionDialog by remember { mutableStateOf(false) } - var deletionInProgress by remember { mutableStateOf(false) } - - val coroutineScope = rememberCoroutineScope() - val mutex = MutatorMutex() - var dropdownExpanded by remember { mutableStateOf(false) } - val isDeletable by remember { - derivedStateOf { - (prescription as? PrescriptionData.Synced)?.isDeletable ?: true - } - } - IconButton( onClick = { dropdownExpanded = true }, modifier = Modifier.testTag(TestTag.Prescriptions.Details.MoreButton) @@ -219,19 +171,20 @@ private fun DeleteAction( DropdownMenu( expanded = dropdownExpanded, onDismissRequest = { dropdownExpanded = false }, - offset = DpOffset(24.dp, 0.dp) + offset = DpOffset(SizeDefaults.triple, SizeDefaults.zero) ) { DropdownMenuItem( modifier = Modifier.testTag(TestTag.Prescriptions.Details.DeleteButton), enabled = isDeletable, onClick = { dropdownExpanded = false - showDeletePrescriptionDialog = true + onClickDelete() } ) { Text( text = stringResource(R.string.pres_detail_dropdown_delete), - color = if (isDeletable) { + color = + if (isDeletable) { AppTheme.colors.red600 } else { AppTheme.colors.neutral400 @@ -239,34 +192,4 @@ private fun DeleteAction( ) } } - - if (showDeletePrescriptionDialog) { - val info = stringResource(R.string.pres_detail_delete_msg) - val cancelText = stringResource(R.string.pres_detail_delete_no) - val actionText = stringResource(R.string.pres_detail_delete_yes) - - CommonAlertDialog( - header = null, - info = info, - cancelText = cancelText, - actionText = actionText, - enabled = !deletionInProgress, - onCancel = { - showDeletePrescriptionDialog = false - }, - onClickAction = { - coroutineScope.launch { - mutex.mutate { - try { - deletionInProgress = true - onClickDelete() - } finally { - showDeletePrescriptionDialog = false - deletionInProgress = false - } - } - } - } - ) - } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailTechnicalInformationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailTechnicalInformationScreen.kt index a67feafe..0fa0d6b8 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailTechnicalInformationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailTechnicalInformationScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -28,12 +28,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag @@ -41,10 +43,13 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes import de.gematik.ti.erp.app.prescription.detail.presentation.rememberPrescriptionDetailController +import de.gematik.ti.erp.app.utils.SpacerMedium import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center class PrescriptionDetailTechnicalInformationScreen( override val navController: NavController, @@ -53,27 +58,44 @@ class PrescriptionDetailTechnicalInformationScreen( @Composable override fun Content() { val taskId = remember { - requireNotNull( - navBackStackEntry.arguments?.getString(PrescriptionDetailRoutes.TaskId) + navBackStackEntry.arguments?.getString( + PrescriptionDetailRoutes.PRESCRIPTION_DETAIL_NAV_TASK_ID ) - } + } ?: "" val prescriptionDetailsController = rememberPrescriptionDetailController(taskId) - val prescriptionState by prescriptionDetailsController.prescriptionState - val listState = rememberLazyListState() - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.Screen), - topBarTitle = stringResource(R.string.pres_detail_technical_information), - listState = listState, - onBack = navController::popBackStack, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - PrescriptionDetailTechnicalInformationScreen( - listState, - innerPadding, - taskId, - prescriptionState?.accessCode - ) - } + val profilePrescriptionData by prescriptionDetailsController.profilePrescription.collectAsStateWithLifecycle() + + UiStateMachine( + state = profilePrescriptionData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + ErrorScreenComponent() + }, + onError = { + ErrorScreenComponent() + }, + onContent = { (_, prescription) -> + val listState = rememberLazyListState() + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.Screen), + topBarTitle = stringResource(R.string.pres_detail_technical_information), + listState = listState, + onBack = navController::popBackStack, + navigationMode = NavigationBarMode.Back + ) { innerPadding -> + PrescriptionDetailTechnicalInformationScreen( + listState, + innerPadding, + prescription.taskId, + prescription.accessCode + ) + } + } + ) } } @@ -82,10 +104,11 @@ private fun PrescriptionDetailTechnicalInformationScreen( listState: LazyListState, innerPadding: PaddingValues, taskId: String, - accessCode: String? + accessCode: String ) { LazyColumn( - modifier = Modifier + modifier = + Modifier .padding(innerPadding) .testTag(TestTag.Prescriptions.Details.TechnicalInformation.Content), state = listState, @@ -94,14 +117,12 @@ private fun PrescriptionDetailTechnicalInformationScreen( item { SpacerMedium() } - accessCode?.let { - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode), - text = it, - label = stringResource(R.string.access_code) - ) - } + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode), + text = accessCode, + label = stringResource(R.string.access_code) + ) } item { Label( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/RedeemFromDetailSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/RedeemFromDetailSection.kt new file mode 100644 index 00000000..0ef6458e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/RedeemFromDetailSection.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.DarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("FunctionNaming") +@Composable +fun RedeemFromDetailSection(onClickRedeemLocal: () -> Unit, onClickRedeemOnline: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = PaddingDefaults.Medium), + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + verticalAlignment = Alignment.CenterVertically + ) { + RedeemSection( + modifier = Modifier.weight(1f), + text = stringResource(R.string.prescription_detail_redeem_online), + image = painterResource(R.drawable.pharmacy_small_32), + onClick = onClickRedeemOnline + ) + + RedeemSection( + modifier = Modifier.weight(1f), + text = stringResource(R.string.prescription_detail_redeem_local), + image = painterResource(R.drawable.dm_code), + onClick = onClickRedeemLocal + ) + } +} + +// TODO: Convert this into a component +@Suppress("FunctionNaming") +@Composable +private fun RedeemSection(modifier: Modifier = Modifier, text: String, image: Painter, onClick: () -> Unit) { + val shape = RoundedCornerShape(SizeDefaults.oneHalf) + + Column( + modifier = modifier + .shadow(elevation = SizeDefaults.half, shape = shape) + .background(AppTheme.colors.neutral050, shape) // using 050 since 025 does not show a difference from screen + .border( + width = SizeDefaults.quarter, + shape = shape, + color = AppTheme.colors.primary600 + ) + .clip(shape) + .clickable( + role = Role.Button, + onClick = onClick + ) + .padding(PaddingDefaults.Small) + .fillMaxWidth() + .fillMaxHeight() + ) { + Image( + image, + null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(SizeDefaults.fourfoldAndHalf) + ) + Text( + text, + modifier = Modifier.align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600 + ) + } +} + +@DarkPreview +@Composable +fun PreviewRedeemFromDetailSection() { + PreviewAppTheme { + RedeemFromDetailSection( + onClickRedeemLocal = {}, + onClickRedeemOnline = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt index 7698ecbd..e2e84429 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui @@ -35,40 +35,45 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.medicationplan.components.MedicationPlanLineItem +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.ui.SentStatusChip import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerShortMedium +import de.gematik.ti.erp.app.utils.SpacerXLarge +import de.gematik.ti.erp.app.utils.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.EditableHeaderTextField import de.gematik.ti.erp.app.utils.compose.HealthPortalLink import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXLarge -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString @Composable fun ScannedPrescriptionOverview( - navController: NavController, listState: LazyListState, prescription: PrescriptionData.Scanned, + medicationSchedule: MedicationSchedule?, + isMedicationPlanEnabled: Boolean, onSwitchRedeemed: (redeemed: Boolean) -> Unit, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit, - onChangePrescriptionName: (String) -> Unit + onChangePrescriptionName: (String) -> Unit, + onClickTechnicalInformation: () -> Unit, + onClickRedeemLocal: () -> Unit, + onShowScannedPrescriptionBottomSheet: () -> Unit, + onClickRedeemOnline: () -> Unit, + onClickMedicationPlan: () -> Unit ) { LazyColumn( state = listState, - modifier = Modifier - .fillMaxSize(), + modifier = + Modifier.fillMaxSize(), contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() ) { item { @@ -78,26 +83,23 @@ fun ScannedPrescriptionOverview( .padding(PaddingDefaults.Medium), horizontalAlignment = Alignment.CenterHorizontally ) { - val titlePrepend = stringResource(R.string.pres_details_scanned_medication) - - val prescriptionName = prescription.name ?: "$titlePrepend ${prescription.index}" - EditableHeaderTextField( - text = prescriptionName, + text = prescription.name, onSaveText = { onChangePrescriptionName(it) } ) SpacerShortMedium() Row( - modifier = Modifier.clickable { - onShowInfo(PrescriptionDetailBottomSheetContent.Scanned()) + modifier = + Modifier.clickable { + onShowScannedPrescriptionBottomSheet() }, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { val date = dateWithIntroductionString(R.string.prs_low_detail_scanned_on, prescription.scannedOn) Text(date, style = AppTheme.typography.body2l) - Icon(Icons.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.primary600) + Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.primary600) } if (prescription.task.communications.isNotEmpty()) { SpacerShortMedium() @@ -106,6 +108,15 @@ fun ScannedPrescriptionOverview( } } + if (!prescription.isRedeemed) { + item { + RedeemFromDetailSection( + onClickRedeemLocal = onClickRedeemLocal, + onClickRedeemOnline = onClickRedeemOnline + ) + } + } + item { Column( Modifier @@ -121,15 +132,16 @@ fun ScannedPrescriptionOverview( SpacerXXLarge() } } + if (isMedicationPlanEnabled) { + item { + MedicationPlanLineItem(medicationSchedule, onClickMedicationPlan) + } + } item { Label( text = stringResource(R.string.pres_detail_technical_information), - onClick = { - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.path(prescription.taskId) - ) - } + onClick = onClickTechnicalInformation ) } @@ -145,11 +157,12 @@ private fun RedeemedButton( redeemed: Boolean, onSwitchRedeemed: (redeemed: Boolean) -> Unit ) { - val buttonText = if (redeemed) { - stringResource(R.string.scanned_prescription_details_mark_as_unredeemed) - } else { - stringResource(R.string.scanned_prescription_details_mark_as_redeemed) - } + val buttonText = + if (redeemed) { + stringResource(R.string.scanned_prescription_details_mark_as_unredeemed) + } else { + stringResource(R.string.scanned_prescription_details_mark_as_redeemed) + } PrimaryButtonSmall( onClick = { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt index f091c08f..8657c3f3 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt @@ -1,49 +1,44 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.detail.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -52,341 +47,394 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.pkv.presentation.rememberInvoiceController +import de.gematik.ti.erp.app.medicationplan.components.MedicationPlanLineItem +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.pkv.presentation.model.InvoiceCardUiState import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.components.SelfPayPrescriptionDetailsChip +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailBottomSheetNavigationData +import de.gematik.ti.erp.app.prescription.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.ui.DirectAssignmentChip import de.gematik.ti.erp.app.prescription.ui.FailureDetailsStatusChip -import de.gematik.ti.erp.app.prescription.ui.PrescriptionStateInfo -import de.gematik.ti.erp.app.prescription.ui.SubstitutionAllowedChip -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.prescription.ui.SubstitutionNotAllowedChip +import de.gematik.ti.erp.app.prescription.ui.components.PrescriptionStateInfo import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerShortMedium import de.gematik.ti.erp.app.utils.compose.HealthPortalLink import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall import de.gematik.ti.erp.app.utils.compose.PrimaryButtonTiny -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.provideEmailIntent -import io.github.aakira.napier.Napier +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod", "LongParameterList") @Composable fun SyncedPrescriptionOverview( - navController: NavController, listState: LazyListState, - consentGranted: Boolean?, - ssoTokenValid: Boolean, - onGrantConsent: () -> Unit, + invoiceCardState: InvoiceCardUiState, + medicationSchedule: MedicationSchedule?, activeProfile: ProfilesUseCaseData.Profile, prescription: PrescriptionData.Synced, + now: Instant = Clock.System.now(), + isMedicationPlanEnabled: Boolean, onClickMedication: (PrescriptionData.Medication) -> Unit, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit + onGrantConsent: () -> Unit, + onClickInvoice: () -> Unit, + onNavigateToRoute: (String) -> Unit, // TODO: remove, hides the navigation + onClickRedeemLocal: () -> Unit, + onClickRedeemOnline: () -> Unit, + onShowInfoBottomSheet: PrescriptionDetailBottomSheetNavigationData, + onShowHowLongValidBottomSheet: () -> Unit, + onClickMedicationPlan: () -> Unit + ) { val noValueText = stringResource(R.string.pres_details_no_value) - - Column { - val colPadding = if (prescription.isIncomplete) { - PaddingValues() - } else { - WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + val isAccident = + remember(prescription) { + prescription.medicationRequest.accidentType != SyncedTaskData.AccidentType.None } - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .testTag(TestTag.Prescriptions.Details.Content), - contentPadding = colPadding - ) { + + LazyColumn( + state = listState, + modifier = + Modifier + .fillMaxWidth() + .testTag(TestTag.Prescriptions.Details.Content), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { PrescriptionName(prescription.name) } + + if (prescription.isIncomplete) { item { - SyncedHeader( - prescription = prescription, - onShowInfo = onShowInfo - ) + SpacerShortMedium() + FailureDetailsStatusChip { + onShowInfoBottomSheet.failureBottomSheet() + } } + } - if (activeProfile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { - item { - InvoiceCardSection( - consentGranted = consentGranted, - onGrantConsent = onGrantConsent, - ssoTokenValid = ssoTokenValid, - navController = navController, - profileId = activeProfile.id, - prescription = prescription - ) + if (prescription.insurance.coverageType == SyncedTaskData.CoverageType.SEL) { + item { + SpacerShortMedium() + SelfPayPrescriptionDetailsChip { + onShowInfoBottomSheet.selPayerPrescriptionBottomSheet() } } + } + if (prescription.isDirectAssignment) { item { - val text = additionalFeeText(prescription.medicationRequest.additionalFee) ?: noValueText - - Label( - text = text, - label = stringResource(R.string.pres_details_additional_fee), - onClick = onClickAdditionalFee( - prescription.medicationRequest.additionalFee, - onShowInfo - ) - ) + SpacerShortMedium() + DirectAssignmentChip { + onShowInfoBottomSheet.directAssignmentBottomSheet() + } + SpacerMedium() + DirectAssignmentInfo(prescription.isDispensed) } + } - prescription.medicationRequest.emergencyFee?.let { emergencyFee -> - item { - Label( - text = stringResource( - if (emergencyFee) R.string.pres_detail_noctu_no else R.string.pres_detail_noctu_yes - ), - label = stringResource(R.string.pres_details_emergency_fee), - onClick = onClickEmergencyFee(emergencyFee, onShowInfo) - ) + if (!prescription.isSubstitutionAllowed) { + item { + SpacerShortMedium() + SubstitutionNotAllowedChip { + onShowInfoBottomSheet.substitutionNotAllowedBottomSheet() } } + } + + item { + SpacerShortMedium() + SyncedPrescriptionStateInfo( + prescriptionState = prescription.state, + now = now, + onClick = when { + prescription.state is SyncedTaskData.SyncedTask.Ready || + prescription.state is SyncedTaskData.SyncedTask.LaterRedeemable + -> { + { + onShowHowLongValidBottomSheet() + } + } + else -> null + } + ) + SpacerShortMedium() + } + + if (activeProfile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.MedicationButton), - text = prescription.name ?: noValueText, - label = stringResource(R.string.pres_details_medication), - onClick = onClickMedication(prescription, onClickMedication, navController) + InvoiceCardSection( + ssoTokenValid = activeProfile.isSSOTokenValid(), + invoiceCardState = invoiceCardState, + onGrantConsent = onGrantConsent, + onClickInvoice = onClickInvoice ) } - + } + if (prescription.state is SyncedTaskData.SyncedTask.Ready && + !prescription.isDirectAssignment && + prescription.redeemState == SyncedTaskData.SyncedTask.RedeemState.RedeemableAndValid + ) { item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.PatientButton), - text = prescription.patient.name ?: noValueText, - label = stringResource(R.string.pres_detail_patient_header), - onClick = { - // TODO: Hoist it out - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailPatientScreen.path(prescription.taskId) - ) - } + RedeemFromDetailSection( + onClickRedeemLocal = onClickRedeemLocal, + onClickRedeemOnline = onClickRedeemOnline ) + SpacerLarge() } + } + if (isMedicationPlanEnabled) { item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriberButton), - text = prescription.practitioner.name ?: noValueText, - label = stringResource(R.string.pres_detail_practitioner_header), - onClick = { - // TODO: Hoist it out - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailPrescriberScreen.path(prescription.taskId) - ) - } - ) + MedicationPlanLineItem(medicationSchedule, onClickMedicationPlan) } + } - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.OrganizationButton), - text = prescription.organization.name ?: noValueText, - label = stringResource(R.string.pres_detail_organization_header), - onClick = { - // TODO: Hoist it out - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailOrganizationScreen.path(prescription.taskId) - ) + item { + val text = additionalFeeText(prescription.medicationRequest.additionalFee) ?: noValueText + Label( + text = text, + label = stringResource(R.string.pres_details_additional_fee), + onClick = { + when (prescription.medicationRequest.additionalFee) { + SyncedTaskData.AdditionalFee.NotExempt -> { + onShowInfoBottomSheet.additionalFeeNotExemptBottomSheet() + } + + SyncedTaskData.AdditionalFee.Exempt -> { + onShowInfoBottomSheet.additionalFeeExemptBottomSheet() + } + + else -> {} } - ) - } + } + ) + } + prescription.medicationRequest.emergencyFee?.let { emergencyFee -> item { Label( - text = stringResource(R.string.pres_detail_accident_header), + text = + stringResource( + if (emergencyFee) R.string.pres_detail_noctu_no else R.string.pres_detail_noctu_yes + ), + label = stringResource(R.string.pres_details_emergency_fee), onClick = { - // TODO: Hoist it out - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailAccidentInfoScreen.path(prescription.taskId) - ) + if (emergencyFee) { + onShowInfoBottomSheet.emergencyFeeNotExemptBottomSheet() + } else { + onShowInfoBottomSheet.emergencyFeeExemptBottomSheet() + } } ) } + } - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformationButton), - text = stringResource(R.string.pres_detail_technical_information), - onClick = { - // TODO: Hoist it out - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.path( - prescription.taskId - ) - ) + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.SubstitutionButton), + text = + stringResource( + if (prescription.isSubstitutionAllowed) { + R.string.prescription_details_substitution_allowed + } else { + R.string.prescription_details_substitution_not_allowed } - ) - } + ), + label = stringResource(R.string.prescription_details_aut_idem_label), + onClick = { + if (prescription.isSubstitutionAllowed) { + onShowInfoBottomSheet.substitutionAllowedBottomSheet() + } else { + onShowInfoBottomSheet.substitutionNotAllowedBottomSheet() + } + } + ) + } - item { - HealthPortalLink( - Modifier.padding( - horizontal = PaddingDefaults.Medium, - vertical = PaddingDefaults.XXLarge - ) - ) - } + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.MedicationButton), + text = prescription.name ?: noValueText, + label = stringResource(R.string.pres_details_medication), + onClick = onClickMedication(prescription, onClickMedication, onNavigateToRoute) + ) } - if (prescription.isIncomplete) { - FailureBanner( - Modifier - .fillMaxWidth() - .navigationBarsPadding(), - prescription + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PatientButton), + text = prescription.patient.name ?: noValueText, + label = stringResource(R.string.pres_detail_patient_header), + onClick = { + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailPatientScreen.path(prescription.taskId) + ) + } ) } - } -} -@Composable -private fun SyncedHeader( - prescription: PrescriptionData.Synced, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -) { - Column( - Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - prescription.name ?: stringResource(R.string.prescription_medication_default_name), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - when { - prescription.isIncomplete -> { - SpacerShortMedium() - FailureDetailsStatusChip( - onClick = { - onShowInfo(PrescriptionDetailBottomSheetContent.Failure()) - } - ) - } + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriberButton), + text = prescription.practitioner.name ?: noValueText, + label = stringResource(R.string.pres_detail_practitioner_header), + onClick = { + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailPrescriberScreen.path(prescription.taskId) + ) + } + ) + } - prescription.isDirectAssignment -> { - SpacerShortMedium() - DirectAssignmentChip( - onClick = { - onShowInfo( - PrescriptionDetailBottomSheetContent.DirectAssignment() - ) - } - ) - } + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.OrganizationButton), + text = prescription.organization.name ?: noValueText, + label = stringResource(R.string.pres_detail_organization_header), + onClick = { + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailOrganizationScreen.path(prescription.taskId) + ) + } + ) + } - prescription.isSubstitutionAllowed -> { - SpacerShortMedium() - SubstitutionAllowedChip( + if (isAccident) { + item { + Label( + text = stringResource(R.string.pres_detail_accident_title), onClick = { - onShowInfo( - PrescriptionDetailBottomSheetContent.SubstitutionAllowed() + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailAccidentInfoScreen.path(prescription.taskId) ) } ) } } - SpacerShortMedium() - - val onClick = when { - !prescription.isDirectAssignment && - ( - prescription.state is SyncedTaskData.SyncedTask.Ready || - prescription.state is SyncedTaskData.SyncedTask.LaterRedeemable - ) -> { - { - onShowInfo( - PrescriptionDetailBottomSheetContent.HowLongValid( - prescription + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformationButton), + text = stringResource(R.string.pres_detail_technical_information), + onClick = { + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailTechnicalInformationScreen.path( + prescription.taskId ) ) } - } + ) + } - else -> null + item { + HealthPortalLink( + Modifier.padding( + horizontal = PaddingDefaults.Medium, + vertical = PaddingDefaults.XXLarge + ) + ) } - SyncedStatus( - prescription = prescription, - onClick = onClick + } + + if (prescription.isIncomplete) { + FailureBanner( + Modifier + .fillMaxWidth(), + prescription ) - SpacerLarge() } } @Composable -private fun SyncedStatus( +fun DirectAssignmentInfo(isDispensed: Boolean) { + val text = + if (isDispensed) { + stringResource(R.string.pres_details_direct_assignment_received_state) + } else { + stringResource(R.string.pres_details_direct_assignment_state) + } + Text( + text, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) +} + +@Composable +fun PrescriptionName(name: String?) { + Text( + name ?: stringResource(R.string.prescription_medication_default_name), + style = AppTheme.typography.h5, + textAlign = TextAlign.Center, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun SyncedPrescriptionStateInfo( modifier: Modifier = Modifier, - prescription: PrescriptionData.Synced, + prescriptionState: SyncedTaskData.SyncedTask.TaskState, + now: Instant, onClick: (() -> Unit)? = null ) { - val clickableModifier = if (onClick != null) { - Modifier - .clickable(role = Role.Button, onClick = onClick) - .padding(start = PaddingDefaults.Tiny) - } else { - Modifier - } + val clickableModifier = + if (onClick != null) { + Modifier + .clickable(role = Role.Button, onClick = onClick) + .padding(start = PaddingDefaults.Tiny) + } else { + Modifier + } - Row( + Column( modifier = modifier - .then(clickableModifier), - verticalAlignment = Alignment.CenterVertically + .then(clickableModifier) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - if (prescription.isDirectAssignment) { - val text = if (prescription.isDispensed) { - stringResource(R.string.pres_details_direct_assignment_received_state) - } else { - stringResource(R.string.pres_details_direct_assignment_state) - } - Text( - text, - style = AppTheme.typography.body2l, + Row(verticalAlignment = Alignment.CenterVertically) { + PrescriptionStateInfo( + state = prescriptionState, + now = now, textAlign = TextAlign.Center ) - } else { - PrescriptionStateInfo(prescription.state, textAlign = TextAlign.Center) - } - if (onClick != null) { - Spacer(Modifier.padding(2.dp)) - Icon( - Icons.Rounded.KeyboardArrowRight, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) + if (onClick != null) { + Spacer(Modifier.padding(SizeDefaults.quarter)) + Icon( + Icons.AutoMirrored.Rounded.KeyboardArrowRight, + null, + modifier = Modifier.size(SizeDefaults.double), + tint = AppTheme.colors.primary600 + ) + } } } } @Composable -private fun additionalFeeText(additionalFee: SyncedTaskData.AdditionalFee): String? = when (additionalFee) { - SyncedTaskData.AdditionalFee.Exempt -> - stringResource(R.string.pres_detail_no) +private fun additionalFeeText(additionalFee: SyncedTaskData.AdditionalFee): String? = + when (additionalFee) { + SyncedTaskData.AdditionalFee.Exempt -> + stringResource(R.string.pres_detail_no) - SyncedTaskData.AdditionalFee.NotExempt -> - stringResource(R.string.pres_detail_yes) + SyncedTaskData.AdditionalFee.NotExempt -> + stringResource(R.string.pres_detail_yes) - else -> null -} + else -> null + } @Composable private fun FailureBanner( @@ -412,11 +460,12 @@ private fun FailureBanner( SpacerMedium() PrimaryButtonTiny( onClick = { - val body = """ - PVS ID: ${prescription.task.pvsIdentifier} - - ${prescription.failureToReport} - """.trimIndent() + val body = + """ + PVS ID: ${prescription.task.pvsIdentifier} + + ${prescription.failureToReport} + """.trimIndent() context.handleIntent( provideEmailIntent( @@ -426,7 +475,8 @@ private fun FailureBanner( ) ) }, - colors = ButtonDefaults.buttonColors( + colors = + ButtonDefaults.buttonColors( backgroundColor = AppTheme.colors.red600, contentColor = AppTheme.colors.neutral000 ) @@ -436,105 +486,59 @@ private fun FailureBanner( } } -@Composable -private fun onClickAdditionalFee( - additionalFee: SyncedTaskData.AdditionalFee, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -): () -> Unit = { - when (additionalFee) { - SyncedTaskData.AdditionalFee.NotExempt -> { - onShowInfo( - PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt() - ) - } - - SyncedTaskData.AdditionalFee.Exempt -> { - onShowInfo( - PrescriptionDetailBottomSheetContent.AdditionalFeeExempt() - ) - } - - else -> {} - } -} - private fun onClickMedication( prescription: PrescriptionData.Synced, onClickMedication: (PrescriptionData.Medication) -> Unit, - navController: NavController -): () -> Unit = { - if (!prescription.isDispensed) { - onClickMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) - } else { - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailMedicationOverviewScreen.path(prescription.taskId) - ) - } -} - -@Composable -private fun onClickEmergencyFee( - emergencyFee: Boolean, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -): () -> Unit = { - if (emergencyFee) { - onShowInfo( - PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt() - ) - } else { - onShowInfo( - PrescriptionDetailBottomSheetContent.EmergencyFee() - ) + onNavigateToRoute: (String) -> Unit +): () -> Unit = + { + if (!prescription.isDispensed) { + onClickMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) + } else { + onNavigateToRoute( + PrescriptionDetailRoutes.PrescriptionDetailMedicationOverviewScreen.path(prescription.taskId) + ) + } } -} @Composable private fun InvoiceCardSection( - consentGranted: Boolean?, - onGrantConsent: () -> Unit, ssoTokenValid: Boolean, - navController: NavController, - profileId: ProfileIdentifier, - prescription: PrescriptionData.Synced + invoiceCardState: InvoiceCardUiState, + onGrantConsent: () -> Unit, + onClickInvoice: () -> Unit ) { - val invoicesController = rememberInvoiceController(profileId = profileId) - val invoice by produceState(null) { - Napier.d { "invoice prescription.taskId ${prescription.taskId}" } - invoicesController.detailState(prescription.taskId).collect { - value = it - } - } - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - PaddingDefaults.Medium - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedVisibility(visible = consentGranted == null && ssoTokenValid) { - InvoiceLoadingCard() - } - AnimatedVisibility(visible = consentGranted != null) { - when { - ssoTokenValid && consentGranted == false -> NoConsentGrantedCard(onGrantConsent) - invoice == null && consentGranted == true -> NoInvoiceConsentGrantedCard() - invoice != null -> PrimaryButtonSmall( - onClick = { - navController.navigate( - PkvRoutes.InvoiceDetailsScreen.path( - taskId = prescription.taskId, - profileId = profileId + AnimatedVisibility(ssoTokenValid) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Crossfade( + modifier = Modifier.animateContentSize(), + targetState = invoiceCardState, + label = "invoice-states" + ) { state -> + when (state) { + InvoiceCardUiState.Loading -> InvoiceLoadingCard() + InvoiceCardUiState.NoConsent -> NoConsentGrantedCard(onGrantConsent) + InvoiceCardUiState.NoInvoice -> NoInvoiceConsentGrantedCard() + InvoiceCardUiState.ShowInvoice -> + PrimaryButtonSmall( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.XLarge) + .wrapContentHeight(), + onClick = onClickInvoice + ) { + Text( + stringResource(R.string.invoice_card_consent_invoice_button_text) ) - ) - } - ) { - Text( - stringResource(R.string.invoice_card_consent_invoice_button_text) - ) + } } } } + SpacerLarge() } - SpacerLarge() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/components/SelfPayerPrescriptionDetailsChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/components/SelfPayerPrescriptionDetailsChip.kt new file mode 100644 index 00000000..b414b847 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/components/SelfPayerPrescriptionDetailsChip.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.ui.StatusChip +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview + +@Composable +fun SelfPayPrescriptionDetailsChip( + modifier: Modifier = Modifier, + onClick: () -> Unit +) = + StatusChip( + modifier = modifier + .clickable(role = Role.Button) { + onClick() + }, + text = stringResource(R.string.pres_details_exp_sel_payer_prescription), + icon = Icons.Outlined.Info, + textColor = AppTheme.colors.primary900, + backgroundColor = AppTheme.colors.primary100, + iconColor = AppTheme.colors.primary600 + ) + +@LightDarkPreview +@Composable +fun PreviewSelfPayPrescriptionDetailsChip() { + SelfPayPrescriptionDetailsChip( + onClick = {} + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/AccidentInfoPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/AccidentInfoPreviewParameter.kt new file mode 100644 index 00000000..c01488a4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/AccidentInfoPreviewParameter.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.datetime.Instant + +data class AccidentInfoPreviewParameter( + val name: String, + val syncedPrescription: PrescriptionData.Synced +) + +class AccidentInfoPreviewParameterProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + AccidentInfoPreviewParameter( + "None", + PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_NONE + ), + AccidentInfoPreviewParameter( + "Accident", + PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_ACCIDENT + ), + AccidentInfoPreviewParameter( + "Work Accident", + PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_WORK_ACCIDENT + ), + AccidentInfoPreviewParameter( + "Occupational Illness", + PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_OCCUPATIONAL_ILLNESS + ) + ) +} + +private val PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_NONE = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = SYNCED_TASK.medicationRequest.copy( + accidentType = SyncedTaskData.AccidentType.None, + dateOfAccident = null, + location = null + ) + ) +) + +val time = Instant.parse("2024-07-03T14:20:00Z") + +private val PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_ACCIDENT = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = SYNCED_TASK.medicationRequest.copy( + accidentType = SyncedTaskData.AccidentType.Unfall, + dateOfAccident = time, + location = "somewhere" + ) + ) +) + +private val PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_WORK_ACCIDENT = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = SYNCED_TASK.medicationRequest.copy( + accidentType = SyncedTaskData.AccidentType.Arbeitsunfall, + dateOfAccident = time, + location = "work" + ) + ) +) + +private val PREVIEW_SYNCED_PRESCRIPTION_ACCIDENT_TYPE_OCCUPATIONAL_ILLNESS = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = SYNCED_TASK.medicationRequest.copy( + accidentType = SyncedTaskData.AccidentType.Berufskrankheit, + dateOfAccident = time, + location = "home" + ) + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/PrescriptionDetailPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/PrescriptionDetailPreviewParameter.kt new file mode 100644 index 00000000..0e2406e7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/preview/PrescriptionDetailPreviewParameter.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UnusedPrivateProperty") + +package de.gematik.ti.erp.app.prescription.detail.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationDispReq +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Medication +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Instant + +data class PrescriptionDetailPreview( + val name: String, // a description for better understanding + val prescription: PrescriptionData.Prescription, + val now: Instant +) + +class PrescriptionDetailPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + PrescriptionDetailPreview( + name = "in_progress_with_accepted_time", + prescription = PREVIEW_SYNCED_PRESCRIPTION, + now = hoursFromLastModifiedDate // for showing the accepted time + ), + PrescriptionDetailPreview( + name = "deleted_with_last_modified_date", + prescription = PREVIEW_SYNCED_PRESCRIPTION.copy( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Canceled + ) + ), + now = hoursFromLastModifiedDate // for showing the accepted time + ), + PrescriptionDetailPreview( + name = "provided_with_last_modified_date", + prescription = PREVIEW_SYNCED_PRESCRIPTION.copy( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.InProgress, + lastMedicationDispense = lastModifiedDate + ) + ), + now = lastModifiedDate + ), + PrescriptionDetailPreview( + name = "in_progress_with_accepted_date", + prescription = PREVIEW_SYNCED_PRESCRIPTION, + now = futureOfLastModifiedDate // for showing accepted date + ), + PrescriptionDetailPreview( + name = "waiting_for_answer_from_pharmacy", + prescription = PREVIEW_SYNCED_PRESCRIPTION.copy( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + currentTime = nowForWaitingState, + communications = listOf( + PREVIEW_REQUEST_COMMUNICATION + ) + ) + ), + now = nowForWaitingState + ), + PrescriptionDetailPreview( + name = "synced_prescription_with_boolean_parameters_true", + prescription = PREVIEW_SYNCED_PRESCRIPTION_BOOLEAN_PARAMETERS_TRUE, + now = futureOfLastModifiedDate + ), + PrescriptionDetailPreview( + name = "synced_prescription_with_boolean_parameters_false", + prescription = PREVIEW_SYNCED_PRESCRIPTION_BOOLEAN_PARAMETERS_FALSE, + now = futureOfLastModifiedDate + ), + PrescriptionDetailPreview( + name = "synced_prescription_as_multiple_prescription", + prescription = PREVIEW_SYNCED_PRESCRIPTION_MULTIPLE_PRESCRIPTION, + now = futureOfLastModifiedDate + ), + PrescriptionDetailPreview( + name = "synced_prescription_as_selfPayer_prescription", + prescription = PREVIEW_SYNCED_PRESCRIPTION.copy( + task = SYNCED_TASK.copy( + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = "Insurance", + status = "Selbstzahler", + coverageType = SyncedTaskData.CoverageType.SEL + ) + ) + ), + now = hoursFromLastModifiedDate // for showing the accepted time + ), + PrescriptionDetailPreview( + name = "scanned_prescription", + prescription = PREVIEW_SCANNED_PRESCRIPTION, + now = futureOfLastModifiedDate + ), + PrescriptionDetailPreview( + name = "scanned_redeemed_prescription", + prescription = PREVIEW_SCANNED_REDEEMED_PRESCRIPTION, + now = futureOfLastModifiedDate + ) + ) +} + +private val lastModifiedDate = Instant.parse("2024-07-03T14:20:00Z") +private val sentOnDate = Instant.parse("2024-07-03T15:20:00Z") // lastModifiedDate < sentOnDate +private val nowForWaitingState = Instant.parse("2024-07-03T14:00:00Z") +private val hoursFromLastModifiedDate = Instant.parse("2024-07-03T15:20:00Z") +private val futureOfLastModifiedDate = Instant.parse("2024-09-03T15:20:00Z") +private val farAwayFutureDate = Instant.parse("3021-11-25T15:20:00Z") + +internal val SYNCED_TASK = SyncedTaskData.SyncedTask( + profileId = "profile-id-1", + taskId = "task-id-1", + accessCode = "access-code-1", + lastModified = lastModifiedDate, // change in this changes the date in the detail screen + organization = SyncedTaskData.Organization( + name = "Muster Apotheke", + address = SyncedTaskData.Address( + line1 = "Musterstraße 1", + line2 = "1. Stock", + postalCode = "12345", + city = "Musterstadt" + ), + uniqueIdentifier = "1234567890", + phone = "0123456789", + mail = "muster@muster.de" + ), + practitioner = Practitioner( + name = "Dr. Max Mustermann", + qualification = "Arzt", + practitionerIdentifier = "1234567890" + ), + patient = Patient( + name = "Max Mustermann", + address = SyncedTaskData.Address( + line1 = "Musterstraße 1", + line2 = "1. Stock", + postalCode = "12345", + city = "Musterstadt" + ), + birthdate = null, + insuranceIdentifier = "1234567890" + ), + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = null, + status = null, + coverageType = SyncedTaskData.CoverageType.GKV + ), + expiresOn = Instant.parse("2028-11-25T15:20:00Z"), + acceptUntil = Instant.parse("3021-11-25T15:20:00Z"), + authoredOn = Instant.parse("2021-11-25T15:20:00Z"), + status = SyncedTaskData.TaskStatus.InProgress, + medicationRequest = SyncedTaskData.MedicationRequest( + medication = Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Ibuprofen", + form = "AEO", + lotNumber = "1234567890", + expirationDate = FhirTemporal.Instant(farAwayFutureDate), + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "KA", + amount = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ), + manufacturingInstructions = null, + packaging = null, + ingredientMedications = emptyList(), + ingredients = emptyList() + ), + dateOfAccident = null, + location = "Musterstadt", + emergencyFee = true, + dosageInstruction = "1-1-1", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "Keine", + substitutionAllowed = true, + additionalFee = SyncedTaskData.AdditionalFee.None + ), + lastMedicationDispense = null, + medicationDispenses = emptyList(), + communications = emptyList(), + failureToReport = "", + isIncomplete = false, + pvsIdentifier = "1234567890" +) + +private val PREVIEW_SYNCED_PRESCRIPTION = PrescriptionData.Synced( + task = SYNCED_TASK +) + +private val PREVIEW_SYNCED_PRESCRIPTION_BOOLEAN_PARAMETERS_TRUE = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = SYNCED_TASK.medicationRequest.copy( + emergencyFee = true, + substitutionAllowed = true, + additionalFee = SyncedTaskData.AdditionalFee.Exempt + ) + ) +) + +private val PREVIEW_SYNCED_PRESCRIPTION_BOOLEAN_PARAMETERS_FALSE = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + medicationRequest = SYNCED_TASK.medicationRequest.copy( + emergencyFee = false, + substitutionAllowed = false, + additionalFee = SyncedTaskData.AdditionalFee.NotExempt + ) + ) +) + +private val PREVIEW_SYNCED_PRESCRIPTION_MULTIPLE_PRESCRIPTION = PrescriptionData.Synced( + task = SYNCED_TASK.copy( + medicationRequest = SYNCED_TASK.medicationRequest.copy( + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo( + indicator = true, + numbering = Ratio( + numerator = Quantity( + value = "1", + unit = "" + ), + denominator = Quantity( + value = "3", + unit = "" + ) + ) + ) + ) + ) +) + +private val PREVIEW_SCANNED_PRESCRIPTION = PrescriptionData.Scanned( + task = ScannedTaskData.ScannedTask( + profileId = "profile-id-1", + taskId = "task-id-1", + scannedOn = Instant.parse("2021-11-25T15:20:00Z"), + index = 1, + name = "Ibuprofen", + accessCode = "access-code-1", + redeemedOn = null + ) +) + +private val PREVIEW_SCANNED_REDEEMED_PRESCRIPTION = PrescriptionData.Scanned( + task = ScannedTaskData.ScannedTask( + profileId = "profile-id-1", + taskId = "task-id-1", + scannedOn = Instant.parse("2021-11-25T15:20:00Z"), + index = 1, + name = "Ibuprofen", + accessCode = "access-code-1", + redeemedOn = Instant.parse("2021-11-25T15:20:00Z") + ) +) + +private val PREVIEW_REQUEST_COMMUNICATION = Communication( + taskId = "task-id-1", + communicationId = "communication-Id-1", + sentOn = sentOnDate, + sender = "pharmacy-Id-1", + consumed = false, + profile = ErxCommunicationDispReq, + orderId = "order-Id-1", + payload = "", + recipient = "Max Mustermann" +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt index 0ec88b76..81bf8279 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.mapper import de.gematik.ti.erp.app.prescription.model.ScannedTaskData.ScannedTask +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.SyncedTask import de.gematik.ti.erp.app.prescription.usecase.model.Prescription @@ -43,6 +44,8 @@ internal fun SyncedTask.toPrescription() = Prescription.SyncedPrescription( state = state(), isDirectAssignment = isDirectAssignment(), prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isSelfPayPrescription = insuranceInformation + .coverageType == SyncedTaskData.CoverageType.SEL, isPartOfMultiplePrescription = medicationRequest .multiplePrescriptionInfo.indicator, numerator = medicationRequest.multiplePrescriptionInfo diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionErrorState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionErrorState.kt new file mode 100644 index 00000000..25ba16e9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionErrorState.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.model + +import de.gematik.ti.erp.app.api.HttpErrorState + +sealed interface PrescriptionErrorState { + + data class HttpError(val errorState: HttpErrorState) : PrescriptionErrorState + data object UnknownError : PrescriptionErrorState +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionScreenData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionScreenData.kt new file mode 100644 index 00000000..cb8774d1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionScreenData.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.model + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData +import de.gematik.ti.erp.app.theme.SizeDefaults + +object PrescriptionScreenData { + @Immutable + data class State( + val prescriptions: List, + val redeemedPrescriptions: List + ) + + sealed class AvatarDimensions { + data class Small( + val dimension: AvatarDimension = AvatarDimension( + SizeDefaults.fivefold, + SizeDefaults.double, + SizeDefaults.doubleHalf, + DpOffset(SizeDefaults.one, SizeDefaults.threeQuarter), + SizeDefaults.quarter, + SizeDefaults.oneHalf + ) + ) : AvatarDimensions() + + data class Default( + val dimension: AvatarDimension = AvatarDimension( + SizeDefaults.twelvefold, + SizeDefaults.triple, + SizeDefaults.fivefold, + DpOffset(SizeDefaults.oneHalf, SizeDefaults.oneHalf), + SizeDefaults.half, + SizeDefaults.double + ) + ) : AvatarDimensions() + } + + data class AvatarDimension( + val avatarSize: Dp, + val chooseSize: Dp, + val statusSize: Dp, + val statusOffset: DpOffset, + val statusBorder: Dp, + val iconSize: Dp + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionGraph.kt index 3584a333..a1ce4633 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.navigation @@ -21,18 +21,43 @@ package de.gematik.ti.erp.app.prescription.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderBottomSheet import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideOutUp import de.gematik.ti.erp.app.prescription.ui.PrescriptionScanScreen +import de.gematik.ti.erp.app.prescription.ui.screen.GrantConsentBottomSheetScreen +import de.gematik.ti.erp.app.prescription.ui.screen.PrescriptionsArchiveScreen +import de.gematik.ti.erp.app.prescription.ui.screen.PrescriptionsScreen +import de.gematik.ti.erp.app.prescription.ui.screen.WelcomeDrawerBottomSheetScreen @Suppress("LongMethod") fun NavGraphBuilder.prescriptionGraph( - startDestination: String = PrescriptionRoutes.PrescriptionScanScreen.route, // TODO Change to PrescriptionScreen + startDestination: String = PrescriptionRoutes.PrescriptionsScreen.route, navController: NavController ) { navigation( startDestination = startDestination, route = PrescriptionRoutes.subGraphName() ) { + renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, + route = PrescriptionRoutes.PrescriptionsScreen.route, + arguments = PrescriptionRoutes.PrescriptionsScreen.arguments + ) { navEntry -> + PrescriptionsScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderComposable( + route = PrescriptionRoutes.PrescriptionsArchiveScreen.route, + arguments = PrescriptionRoutes.PrescriptionsArchiveScreen.arguments + ) { navEntry -> + PrescriptionsArchiveScreen(navController, navEntry) + } renderComposable( route = PrescriptionRoutes.PrescriptionScanScreen.route, arguments = PrescriptionRoutes.PrescriptionScanScreen.arguments @@ -42,5 +67,20 @@ fun NavGraphBuilder.prescriptionGraph( navBackStackEntry = navEntry ) } + renderBottomSheet( + route = PrescriptionRoutes.WelcomeDrawerBottomSheetScreen.route, + arguments = PrescriptionRoutes.WelcomeDrawerBottomSheetScreen.arguments + ) { navEntry -> + WelcomeDrawerBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = PrescriptionRoutes.GrantConsentBottomSheetScreen.route, + arguments = PrescriptionRoutes.GrantConsentBottomSheetScreen.arguments + ) { navEntry -> + GrantConsentBottomSheetScreen(navController, navEntry) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionRoutes.kt index 931ae535..874f94e1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/navigation/PrescriptionRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.navigation @@ -23,7 +23,11 @@ import de.gematik.ti.erp.app.navigation.NavigationRoutes import de.gematik.ti.erp.app.navigation.Routes object PrescriptionRoutes : NavigationRoutes { - override fun subGraphName() = "main:scanner" // TODO Change to main when all prescriptions are refactored + override fun subGraphName() = "prescriptions" + object PrescriptionsScreen : Routes(NavigationRouteNames.PrescriptionsScreen.name) + object PrescriptionsArchiveScreen : Routes(NavigationRouteNames.PrescriptionsArchiveScreen.name) object PrescriptionScanScreen : Routes(NavigationRouteNames.PrescriptionScanScreen.name) + object WelcomeDrawerBottomSheetScreen : Routes(NavigationRouteNames.WelcomeDrawerBottomSheetScreen.name) + object GrantConsentBottomSheetScreen : Routes(NavigationRouteNames.GrantConsentBottomSheetScreen.name) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsController.kt index 98531e5f..dfd57c68 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsController.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("UnusedPrivateMember") package de.gematik.ti.erp.app.prescription.presentation @@ -22,41 +23,291 @@ package de.gematik.ti.erp.app.prescription.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.analytics.model.TrackedEvent.ArchivePrescriptionCount +import de.gematik.ti.erp.app.analytics.model.TrackedEvent.ScannedPrescriptionCount +import de.gematik.ti.erp.app.analytics.model.TrackedEvent.SyncedPrescriptionCount +import de.gematik.ti.erp.app.analytics.tracker.Tracker +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.httpErrorState +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.ChooseAuthenticationController +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import de.gematik.ti.erp.app.base.usecase.DownloadAllResourcesUseCase +import de.gematik.ti.erp.app.consent.usecase.ShowGrantConsentDrawerUseCase +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.mainscreen.model.MultiProfileAppBarFlowWrapper +import de.gematik.ti.erp.app.prescription.model.PrescriptionErrorState import de.gematik.ti.erp.app.prescription.usecase.GetActivePrescriptionsUseCase import de.gematik.ti.erp.app.prescription.usecase.GetArchivedPrescriptionsUseCase -import de.gematik.ti.erp.app.profiles.presentation.ProfileController.Companion.DEFAULT_EMPTY_PROFILE +import de.gematik.ti.erp.app.prescription.usecase.GetDownloadResourcesSnapshotStateUseCase +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.Companion.countByType import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase -import kotlinx.coroutines.CoroutineScope +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.redeem.usecase.HasRedeemableTasksUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveToolTipsShownUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isNotDataState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly +import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance +private val TAG = PrescriptionsController::class.qualifiedName.toString() + +@Suppress("ConstructorParameterNaming", "LongParameterList") @Stable class PrescriptionsController( + getProfileByIdUseCase: GetProfileByIdUseCase, + getProfilesUseCase: GetProfilesUseCase, + chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + snapshotStateUseCase: GetDownloadResourcesSnapshotStateUseCase, + biometricAuthenticator: BiometricAuthenticator, + private val tracker: Tracker, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val downloadAllResourcesUseCase: DownloadAllResourcesUseCase, private val activePrescriptionsUseCase: GetActivePrescriptionsUseCase, private val archivedPrescriptionsUseCase: GetArchivedPrescriptionsUseCase, - private val profileId: ProfileIdentifier, - scope: CoroutineScope + private val getMLKitAcceptedUseCase: GetMLKitAcceptedUseCase, + private val getShowWelcomeDrawerUseCase: GetShowWelcomeDrawerUseCase, + private val showGrantConsentDrawerUseCase: ShowGrantConsentDrawerUseCase, + private val saveToolTipsShownUseCase: SaveToolTipsShownUseCase, + private val switchActiveProfileUseCase: SwitchActiveProfileUseCase, + private val hasRedeemableTasksUseCase: HasRedeemableTasksUseCase, + // events + val refreshEvent: ComposableEvent = ComposableEvent(), + val onUserNotAuthenticatedErrorEvent: ComposableEvent = ComposableEvent(), + private val localPrescriptionsRefreshTrigger: MutableStateFlow = MutableStateFlow(false) + +) : ChooseAuthenticationController( + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator, + onActiveProfileSuccess = { _, scope -> + scope.launch { + localPrescriptionsRefreshTrigger.value = !localPrescriptionsRefreshTrigger.value + } + } ) { - private val activePrescriptions by lazy { - activePrescriptionsUseCase(profileId).stateIn(scope, Eagerly, emptyList()) + private val _isProfileRefreshing: MutableStateFlow = MutableStateFlow(false) + + init { + biometricAuthenticationSuccessEvent.listen(controllerScope) { refreshDownload() } + + biometricAuthenticationResetErrorEvent.listen(controllerScope) { error -> + if (error is AuthenticationResult.IdpCommunicationError.UserNotAuthenticated) { + onUserNotAuthenticatedErrorEvent.trigger() + } + } + + onRefreshProfileAction.listen(controllerScope) { isRefreshing -> + updateProfileRefreshingState(isRefreshing) + } + } + + private fun loadPrescriptions() { + localPrescriptionsRefreshTrigger.value = !localPrescriptionsRefreshTrigger.value + } + + private fun updateProfileRefreshingState(isRefreshing: Boolean) { + _isProfileRefreshing.value = isRefreshing + } + + private fun downloadAllResources() { + controllerScope.launch { + val profile = activeProfile.extract() + if (profile != null && profile.isSSOTokenValid()) { + disableProfileRefresh() + enablePrescriptionsRefresh() + downloadAllResourcesUseCase.invoke(profile.id) + .fold( + onSuccess = { numberOfNewPrescriptions -> + Napier.d { "Download successful with $numberOfNewPrescriptions prescriptions" } + loadPrescriptions() + }, + onFailure = { exception -> + handleError(exception) { error -> + loadPrescriptions() + disablePrescriptionRefresh() + Napier.e { "error: $error" } + } + } + ) + } + } + } + + private fun disableProfileRefresh() { + onRefreshProfileAction.trigger(false) + } + + private fun enablePrescriptionsRefresh() { + refreshEvent.trigger(true) + } + + fun disablePrescriptionRefresh() { + refreshEvent.trigger(false) + } + + private fun handleError(error: Throwable, tag: String = TAG, onFailure: ((PrescriptionErrorState) -> Unit)? = null) { + val errorState: PrescriptionErrorState = when (error) { + is ApiCallException -> PrescriptionErrorState.HttpError(errorState = error.response.httpErrorState()) + else -> PrescriptionErrorState.UnknownError + } + Napier.e(tag = tag) { "Error on download: ${error.stackTraceToString()}" } + onFailure?.invoke(errorState) + } + + fun refreshDownload() { + downloadAllResources() + } + + fun saveToolTipsShown() { + controllerScope.launch { + saveToolTipsShownUseCase() + } + } + + fun switchActiveProfile(id: ProfileIdentifier) { + controllerScope.launch { + switchActiveProfileUseCase.invoke(id) + } } - private val archivedPrescriptions by lazy { - archivedPrescriptionsUseCase(profileId).stateIn(scope, Eagerly, emptyList()) + @OptIn(ExperimentalCoroutinesApi::class) + val archivedPrescriptions: StateFlow>> by lazy { + localPrescriptionsRefreshTrigger + .flatMapLatest { + activeProfile.extract()?.id?.let { selectedProfileId -> + archivedPrescriptionsUseCase.invoke(selectedProfileId).map { + when { + it.isEmpty() -> UiState.Empty() + else -> UiState.Data(it) + } + } + } ?: emptyFlow() + }.stateIn( + scope = controllerScope, + initialValue = UiState.Loading(), + started = SharingStarted.WhileSubscribed() + ) } - val activePrescriptionsState - @Composable - get() = activePrescriptions.collectAsStateWithLifecycle(emptyList()) - val archivedPrescriptionsState - @Composable - get() = archivedPrescriptions.collectAsStateWithLifecycle(emptyList()) + private val isArchiveDataEmpty by lazy { + archivedPrescriptions.map { it.data?.isEmpty() == true } + } + + private val isArchiveNotLoaded by lazy { + archivedPrescriptions.map { it.isNotDataState } + } + + @OptIn(ExperimentalCoroutinesApi::class) + val activePrescriptions: StateFlow>> by lazy { + localPrescriptionsRefreshTrigger + .flatMapLatest { + activeProfile.extract()?.id?.let { selectedProfileId -> + activePrescriptionsUseCase.invoke(selectedProfileId).map { UiState.Data(it) } + } ?: emptyFlow() + }.stateIn( + scope = controllerScope, + initialValue = UiState.Loading(), + started = SharingStarted.WhileSubscribed() + ) + } + + val isArchiveEmpty: StateFlow by lazy { + combine(isArchiveDataEmpty, isArchiveNotLoaded) { isEmpty, isNotLoaded -> + isEmpty || isNotLoaded + }.stateIn( + scope = controllerScope, + initialValue = true, + started = SharingStarted.WhileSubscribed() + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + val hasRedeemableTasks by lazy { + getActiveProfileUseCase().flatMapLatest { profile -> + hasRedeemableTasksUseCase(profile.id) + }.stateIn( + scope = controllerScope, + initialValue = false, + started = SharingStarted.WhileSubscribed() + ) + } + + val isMLKitAccepted by lazy { + getMLKitAcceptedUseCase().stateIn(controllerScope, Lazily, false) + } + + val shouldShowWelcomeDrawer by lazy { getShowWelcomeDrawerUseCase() } + + val shouldShowGrantConsentDrawer by lazy { showGrantConsentDrawerUseCase() } + + val resourcesDownloadedState: SharedFlow = snapshotStateUseCase() + + val multiProfileData by lazy { + MultiProfileAppBarFlowWrapper( + existingProfiles = getProfilesUseCase() + .stateIn(controllerScope, Eagerly, listOf(MultiProfileAppBarFlowWrapper.DEFAULT_EMPTY_PROFILE)), + activeProfile = getActiveProfileUseCase() + .stateIn(controllerScope, Eagerly, MultiProfileAppBarFlowWrapper.DEFAULT_EMPTY_PROFILE), + isProfileRefreshing = _isProfileRefreshing.asStateFlow() + ) + } + + fun trackPrescriptionCounts() { + controllerScope.launch { + with(tracker) { + activePrescriptions.syncedCount()?.let { trackEvent(SyncedPrescriptionCount(it)) } + activePrescriptions.scannedCount()?.let { trackEvent(ScannedPrescriptionCount(it)) } + archivedPrescriptions.count()?.let { trackEvent(ArchivePrescriptionCount(it)) } + } + } + } + + companion object { + private suspend fun StateFlow>.extract(): Profile? = first { it.isDataState }.data + + private suspend fun StateFlow>>.syncedCount(): Int? = + countByType(Prescription.SyncedPrescription::class.java) + + private suspend fun StateFlow>>.scannedCount(): Int? = + countByType(Prescription.ScannedPrescription::class.java) + + private suspend fun StateFlow>>.count(): Int? = + first { it.isDataState }.data?.takeIf { it.isNotEmpty() }?.count() + } } @Composable @@ -64,16 +315,41 @@ fun rememberPrescriptionsController(): PrescriptionsController { val activePrescriptionsUseCase by rememberInstance() val archivedPrescriptionsUseCase by rememberInstance() val getActiveProfileUseCase by rememberInstance() - val scope = rememberCoroutineScope() + val switchActiveProfileUseCase by rememberInstance() + val mlKitAcceptedUseCase by rememberInstance() + val getShowWelcomeDrawerUseCase by rememberInstance() + val showGrantConsentDrawerUseCase by rememberInstance() + val saveWelcomeDrawerShownUseCase by rememberInstance() + val saveToolTipsShownUseCase by rememberInstance() + val getProfileByIdUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val downloadAllResourcesUseCase by rememberInstance() + val chooseAuthenticationDataUseCase by rememberInstance() + val getDownloadResourcesSnapshotStateUseCase by rememberInstance() + val tracker by rememberInstance() + val hasRedeemableTasksUseCase by rememberInstance() - val activeProfile by getActiveProfileUseCase().collectAsStateWithLifecycle(DEFAULT_EMPTY_PROFILE) + val biometricAuthenticator = LocalBiometricAuthenticator.current + val activeProfile by getActiveProfileUseCase().collectAsStateWithLifecycle(null) - return rememberSaveable(activeProfile.id, saver = complexAutoSaver()) { + return rememberSaveable(activeProfile, saver = complexAutoSaver()) { PrescriptionsController( - profileId = activeProfile.id, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, activePrescriptionsUseCase = activePrescriptionsUseCase, archivedPrescriptionsUseCase = archivedPrescriptionsUseCase, - scope = scope + getMLKitAcceptedUseCase = mlKitAcceptedUseCase, + showGrantConsentDrawerUseCase = showGrantConsentDrawerUseCase, + getShowWelcomeDrawerUseCase = getShowWelcomeDrawerUseCase, + saveToolTipsShownUseCase = saveToolTipsShownUseCase, + downloadAllResourcesUseCase = downloadAllResourcesUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + snapshotStateUseCase = getDownloadResourcesSnapshotStateUseCase, + tracker = tracker, + hasRedeemableTasksUseCase = hasRedeemableTasksUseCase, + biometricAuthenticator = biometricAuthenticator ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/RefreshPrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/RefreshPrescriptionsController.kt index 08e17700..1329aa95 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/RefreshPrescriptionsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/RefreshPrescriptionsController.kt @@ -1,29 +1,34 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.api.GeneralErrorState +import de.gematik.ti.erp.app.api.RefreshedState import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator import de.gematik.ti.erp.app.cardwall.mini.ui.NoneEnrolledException @@ -32,11 +37,9 @@ import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.idp.usecase.IDPConfigException import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.ui.RefreshedState +import de.gematik.ti.erp.app.mainscreen.presentation.AppController import de.gematik.ti.erp.app.prescription.usecase.RefreshPrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.RefreshState import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.vau.interceptor.VauException import io.github.aakira.napier.Napier @@ -58,12 +61,12 @@ import java.net.UnknownHostException @Stable class RefreshPrescriptionsController( private val refreshPrescriptionUseCase: RefreshPrescriptionUseCase, - private val mainScreenController: MainScreenController, + private val appController: AppController, private val authenticator: Authenticator, private val scope: CoroutineScope ) { - val isRefreshing + val isRefreshing: State @Composable get() = refreshPrescriptionUseCase.refreshInProgress.collectAsStateWithLifecycle() @@ -83,11 +86,12 @@ class RefreshPrescriptionsController( GeneralErrorState.NoneEnrolled -> { onShowCardWall() } + GeneralErrorState.UserNotAuthenticated -> { onUserNotAuthenticated() } + else -> { - mainScreenController.onRefresh(finalState) } } } @@ -96,44 +100,44 @@ class RefreshPrescriptionsController( private fun refreshFlow( profileId: ProfileIdentifier, isUserAction: Boolean - ): Flow = + ): Flow = refreshPrescriptionUseCase.downloadFlow(profileId) - .map { - RefreshedState(it) as PrescriptionServiceState - } + .map { RefreshedState(it) } .retryWithAuthenticator( isUserAction = isUserAction, - authenticate = authenticator.authenticateForPrescriptions(profileId) + authenticate = authenticator.getAuthResult(profileId) ) .catchAndTransformRemoteExceptions() .flowOn(Dispatchers.IO) } @Composable -fun rememberRefreshPrescriptionsController(mainScreenController: MainScreenController): RefreshPrescriptionsController { +fun rememberRefreshPrescriptionsController(appController: AppController): RefreshPrescriptionsController { val refreshPrescriptionUseCase by rememberInstance() val authenticator = LocalAuthenticator.current val scope = rememberCoroutineScope() return remember { RefreshPrescriptionsController( refreshPrescriptionUseCase = refreshPrescriptionUseCase, - mainScreenController = mainScreenController, + appController = appController, authenticator = authenticator, scope = scope ) } } -fun Flow.retryWithAuthenticator( +fun Flow.retryWithAuthenticator( isUserAction: Boolean, authenticate: Flow ) = +// Retries collection of the given flow up to retries times when an exception that + // matches the given predicate occurs in the upstream flow. retry(1) { throwable -> Napier.d("Retry with authenticator", throwable) when { - !isUserAction -> - throw CancellationException("Authentication cancelled due `isUserAction = false`") + !isUserAction -> throw CancellationException("Authentication cancelled due `isUserAction = false`") + (throwable.cause as? RefreshFlowException)?.isUserAction == true -> { authenticate .first() @@ -142,20 +146,24 @@ fun Flow.retryWithAuthenticator( PromptAuthenticator.AuthResult.Authenticated -> true PromptAuthenticator.AuthResult.Cancelled -> throw CancellationException("Authentication dialog cancelled") + PromptAuthenticator.AuthResult.NoneEnrolled -> throw NoneEnrolledException() + PromptAuthenticator.AuthResult.UserNotAuthenticated -> throw UserNotAuthenticatedException() + PromptAuthenticator.AuthResult.RedirectLinkNotRight -> throw RedirectUrlWrongException() } } } + else -> false } } -fun Flow.catchAndTransformRemoteExceptions() = +fun Flow.catchAndTransformRemoteExceptions() = catch { throwable -> Napier.d("Try to transform exception", throwable) @@ -165,24 +173,23 @@ fun Flow.catchAndTransformRemoteExceptions() = private fun Throwable.walkCause(): GeneralErrorState? = cause?.walkCause() ?: transformException() -private fun Throwable.transformException(): GeneralErrorState? = - when (this) { - is RedirectUrlWrongException -> // TODO Do something with this - GeneralErrorState.RedirectUrlForExternalAuthenticationWrong - is UserNotAuthenticatedException -> - GeneralErrorState.UserNotAuthenticated - is NoneEnrolledException -> - GeneralErrorState.NoneEnrolled - is VauException -> - GeneralErrorState.FatalTruststoreState - is IDPConfigException -> // TODO use other state - GeneralErrorState.FatalTruststoreState - is SocketTimeoutException, - is UnknownHostException -> - GeneralErrorState.NetworkNotAvailable - is ApiCallException -> - GeneralErrorState.ServerCommunicationFailedWhileRefreshing( - this.response.code() - ) +@Requirement( + "O.Plat_4#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The error that occurs is captured and transformed. " + + "The sensitive data that might come with the errors are transformed so that it is not transmitted." +) +private fun Throwable.transformException(): GeneralErrorState? { + Napier.e { "Exception transformed $this" } + + return when (this) { + is RedirectUrlWrongException -> GeneralErrorState.RedirectUrlForExternalAuthenticationWrong + is UserNotAuthenticatedException -> GeneralErrorState.UserNotAuthenticated + is NoneEnrolledException -> GeneralErrorState.NoneEnrolled + is VauException -> GeneralErrorState.FatalTruststoreState + is IDPConfigException -> GeneralErrorState.FatalTruststoreState + is SocketTimeoutException, is UnknownHostException -> GeneralErrorState.NetworkNotAvailable + is ApiCallException -> GeneralErrorState.ServerCommunicationFailedWhileRefreshing(this.response.code()) else -> null } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/ScanPrescriptionController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/ScanPrescriptionController.kt index 9d12c8ec..fd4b0f86 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/ScanPrescriptionController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/ScanPrescriptionController.kt @@ -1,28 +1,32 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.presentation +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.ui.ScannedCode import de.gematik.ti.erp.app.prescription.ui.TwoDCodeProcessor import de.gematik.ti.erp.app.prescription.ui.TwoDCodeScanner @@ -87,9 +91,15 @@ class ScanPrescriptionController( val processor: TwoDCodeProcessor, private val validator: TwoDCodeValidator, dispatchers: DispatchProvider, + private val context: Context, private val scope: CoroutineScope ) { - + @Requirement( + "O.Data_6#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Collected data is stored only till the lifecycle of the viewmodel", + codeLines = 3 + ) private val scannedCodes = MutableStateFlow(listOf()) var vibration = MutableSharedFlow() private set @@ -229,7 +239,10 @@ class ScanPrescriptionController( getActiveProfileUseCase().collectLatest { profile -> prescriptionUseCase.saveScannedCodes( profile.id, - scannedCodes.value + scannedCodes.value, + context.getString( + R.string.pres_details_scanned_medication + ) ) } } @@ -244,6 +257,7 @@ fun rememberScanPrescriptionController(): ScanPrescriptionController { val processor by rememberInstance() val validator by rememberInstance() val dispatchers by rememberInstance() + val context = LocalContext.current val scope = rememberCoroutineScope() return remember { ScanPrescriptionController( @@ -253,6 +267,7 @@ fun rememberScanPrescriptionController(): ScanPrescriptionController { processor = processor, validator = validator, dispatchers = dispatchers, + context = context, scope ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/WelcomeDrawerController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/WelcomeDrawerController.kt new file mode 100644 index 00000000..212a0f5c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/presentation/WelcomeDrawerController.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.prescription.usecase.SaveWelcomeMessageUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class WelcomeDrawerController( + private val saveWelcomeDrawerShownUseCase: SaveWelcomeDrawerShownUseCase, + private val saveWelcomeMessageUseCase: SaveWelcomeMessageUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase +) : GetActiveProfileController(getActiveProfileUseCase) { + + init { + controllerScope.launch { + saveWelcomeMessageUseCase() + } + } + + fun onWelcomeDrawerShown() { + controllerScope.launch { + saveWelcomeDrawerShownUseCase() + } + } +} + +@Composable +fun rememberWelcomeDrawerController(): WelcomeDrawerController { + val saveWelcomeDrawerShownUseCase by rememberInstance() + val saveWelcomeMessageUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + return remember { + WelcomeDrawerController( + saveWelcomeDrawerShownUseCase = saveWelcomeDrawerShownUseCase, + saveWelcomeMessageUseCase = saveWelcomeMessageUseCase, + getActiveProfileUseCase = getActiveProfileUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/DownloadResourcesStateRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/DownloadResourcesStateRepository.kt new file mode 100644 index 00000000..31e1a616 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/DownloadResourcesStateRepository.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.repository + +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * Repository for number of new prescription obtained from server on every download or refresh + */ +class DownloadResourcesStateRepository( + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) +) { + private val currentState = MutableStateFlow(DownloadResourcesState.NotStarted) + private val snapshotState = MutableSharedFlow() + + fun updateDetailState(state: DownloadResourcesState) { + currentState.value = state + } + + fun updateSnapshotState(state: DownloadResourcesState) { + scope.launch { + snapshotState.emit(state) + } + } + + fun closeSnapshotState() { + scope.launch { + snapshotState.emit(DownloadResourcesState.Finished) + } + } + + fun snapshotState(): SharedFlow = snapshotState.asSharedFlow() + + fun detailState(): StateFlow = currentState.asStateFlow() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt index c6708c7f..047a64e0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -96,7 +96,7 @@ val codeToFormMapping = mapOf( "GLO" to R.string.kbv_code_dosage_form_glo, "GMR" to R.string.kbv_code_dosage_form_gmr, "GPA" to R.string.kbv_code_dosage_form_gpa, - "GRA" to R.string.kbv_code_dosage_form_gra, + "GRA" to R.string.kbv_code_dosage_form_gra, // todo rename in Lokalize "GSE" to R.string.kbv_code_dosage_form_gse, "GUL" to R.string.kbv_code_dosage_form_gul, "HAS" to R.string.kbv_code_dosage_form_has, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt deleted file mode 100644 index 0f7a5bf8..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes -import de.gematik.ti.erp.app.prescription.presentation.PrescriptionsController -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter - -@RequiresApi(Build.VERSION_CODES.O) -@Composable -fun ArchiveScreen( - prescriptionsController: PrescriptionsController, - navController: NavController, - onBack: () -> Unit -) { - val listState = rememberLazyListState() - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.archive_screen_title), - listState = listState, - onBack = onBack, - navigationMode = NavigationBarMode.Back - ) { - val archivedPrescriptions by prescriptionsController.archivedPrescriptionsState - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.Prescriptions.Archive.Content), - state = listState, - horizontalAlignment = Alignment.CenterHorizontally - ) { - item { SpacerXXLarge() } - - archivedPrescriptions.forEachIndexed { index, prescription -> - item(key = "prescription-${prescription.taskId}") { - val previousPrescriptionRedeemedOn = - archivedPrescriptions.getOrNull(index - 1) - ?.redeemedOrExpiredOn() - ?.toLocalDateTime(TimeZone.currentSystemDefault()) - - val redeemedOn = prescription.redeemedOrExpiredOn() - .toLocalDateTime(TimeZone.currentSystemDefault()) - - val yearChanged = remember { - previousPrescriptionRedeemedOn?.year != redeemedOn.year - } - - if (yearChanged) { - val instantOfArchivedPrescription = remember { - val dateFormatter = DateTimeFormatter.ofPattern("yyyy") - redeemedOn.toJavaLocalDateTime().format(dateFormatter) - } - - Text( - text = instantOfArchivedPrescription, - style = AppTheme.typography.h6, - modifier = Modifier - .fillMaxWidth() - .then(CardPaddingModifier) - ) - } - - when (prescription) { - is ScannedPrescription -> - LowDetailMedication( - modifier = CardPaddingModifier, - prescription, - onClick = { - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailScreen.path( - taskId = prescription.taskId - ) - ) - } - ) - - is SyncedPrescription -> - FullDetailMedication( - prescription, - modifier = CardPaddingModifier, - onClick = { - navController.navigate( - PrescriptionDetailRoutes.PrescriptionDetailScreen.path( - taskId = prescription.taskId - ) - ) - } - ) - } - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt deleted file mode 100644 index 5a6a4fda..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -@Composable -fun EmptyScreenHome( - modifier: Modifier = Modifier, - header: String, - description: String, - image: @Composable () -> Unit, - button: @Composable () -> Unit -) { - Column( - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - ) { - image() - SpacerMedium() - Text( - text = header, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth().wrapContentHeight() - ) - SpacerSmall() - Text( - text = description, - style = AppTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth().wrapContentHeight() - ) - SpacerSmall() - button() - } -} - -@Composable -fun EmptyScreenArchive( - modifier: Modifier = Modifier, - header: String, - description: String -) { - Column( - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - ) { - Text( - text = header, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - SpacerSmall() - Text( - text = description, - style = AppTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } -} - -@Composable -fun ConnectButton( - onClick: () -> Unit -) = - TextButton( - onClick = onClick - ) { - Icon( - Icons.Rounded.Refresh, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = "Verbinden", textAlign = TextAlign.Right) - } - -@Composable -fun RefreshButton( - onClick: () -> Unit -) = - TextButton( - onClick = onClick - ) { - Icon( - Icons.Rounded.Refresh, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = "Aktualisieren", textAlign = TextAlign.Right) - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt deleted file mode 100644 index 4dd2e343..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.QrCode -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall - -@Composable -fun HomeConnectedWithoutTokenBiometrics( - modifier: Modifier = Modifier, - onClickAction: () -> Unit -) = - EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.main_empty_screen_connect_now_title), - description = stringResource(R.string.main_empty_screen_connect_now), - image = { - Image( - painterResource(R.drawable.clapping_hands_blue), - contentDescription = null, - modifier = Modifier.size(160.dp) - ) - }, - button = { - ConnectButton( - onClick = onClickAction - ) - } - ) - -@Composable -fun HomeConnectedWithoutToken( - modifier: Modifier = Modifier, - onClickAction: () -> Unit -) = - EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.main_empty_screen_tokens_removed_connect_now_title), - description = stringResource(R.string.main_empty_screen_tokens_removed_connect_now), - image = { - Image( - painterResource(R.drawable.girl_red_oh_no), - contentDescription = null, - modifier = Modifier.size(160.dp) - ) - }, - button = { - ConnectButton( - onClick = onClickAction - ) - } - ) - -@Composable -fun HomeHealthCardConnected( - modifier: Modifier = Modifier, - onClickAction: () -> Unit -) = - EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.home_egk_redeemed_header), - description = stringResource(R.string.home_egk_redeemed_description), - image = { - Image( - painterResource(R.drawable.woman_red_shirt_circle_blue), - contentDescription = null, - modifier = Modifier.size(160.dp) - ) - }, - button = { - TextButton( - onClick = onClickAction - ) { - Icon( - Icons.Rounded.Refresh, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = stringResource(R.string.home_egk_redeemed_buttontext), textAlign = TextAlign.Right) - } - } - ) - -@Preview -@Composable -private fun HomeHealthCardConnectedPreview() { - AppTheme { - HomeHealthCardConnected(onClickAction = {}) - } -} - -@Composable -fun HomeHealthCardDisconnected( - modifier: Modifier = Modifier, - onClickAction: () -> Unit -) = - EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.home_egk_notredeemable_header), - description = stringResource(R.string.home_egk_notredeemable_description), - image = { - Image( - painterResource(R.drawable.alarm_clock), - contentDescription = null - ) - }, - button = { - TextButton( - onClick = onClickAction - ) { - Icon( - Icons.Rounded.Refresh, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = stringResource(R.string.home_egk_notredeemable_buttontext), textAlign = TextAlign.Right) - } - } - ) - -@Preview -@Composable -private fun HomeHealthCardDisconnectedPreview() { - AppTheme { - HomeHealthCardDisconnected(onClickAction = {}) - } -} - -@Composable -fun HomeNoHealthCard( - modifier: Modifier = Modifier, - onClickAction: () -> Unit -) = EmptyScreenHome( - modifier = modifier, - header = stringResource(R.string.home_noegk_initial_header), - description = stringResource(R.string.home_noegk_initial_description), - image = { - Image( - painterResource(R.drawable.prescription), - contentDescription = null - ) - }, - button = { - TextButton(onClick = onClickAction) { - Icon( - imageVector = Icons.Rounded.QrCode, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - SpacerSmall() - Text(text = stringResource(R.string.home_noegk_initial_buttontext), textAlign = TextAlign.Right) - } - } -) - -@Preview -@Composable -private fun HomeNoHealthCardPreview() { - AppTheme { - HomeNoHealthCard(onClickAction = {}) - } -} - -@Composable -fun ArchiveNoHealthCardInitial(modifier: Modifier = Modifier) = EmptyScreenArchive( - modifier = modifier, - header = stringResource(R.string.archive_noegk_initial_header), - description = stringResource(R.string.archive_noegk_initial_description) -) - -@Composable -fun ArchiveNoHealthCardRedeemed(modifier: Modifier = Modifier) = EmptyScreenArchive( - modifier = modifier, - header = stringResource(R.string.archive_noegk_redeemed_header), - description = stringResource(R.string.archive_noegk_redeemed_description) -) - -@Preview -@Composable -private fun ArchiveNoEGKInitialPreview() { - AppTheme { - ArchiveNoHealthCardInitial() - } -} - -@Preview -@Composable -private fun ArchiveNoEGKRedeemedPreview() { - AppTheme { - ArchiveNoHealthCardRedeemed() - } -} - -@Composable -fun HomeNoHealthCardSignInHint(onClickAction: () -> Unit) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RectangleShape, - backgroundColor = AppTheme.colors.neutral050 - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) { - Text( - text = stringResource(R.string.home_noegk_signin_hint_description), - modifier = Modifier - .padding(vertical = PaddingDefaults.Medium) - .fillMaxWidth() - .weight(1f), - style = AppTheme.typography.body2 - ) - TextButton( - onClick = onClickAction, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors(backgroundColor = AppTheme.colors.primary600), - modifier = Modifier.align(Alignment.CenterVertically).testTag(TestTag.Main.LoginButton), - contentPadding = PaddingValues(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny) - ) { - Text( - text = stringResource(R.string.home_noegk_signin_hint_buttontext), - textAlign = TextAlign.Center, - style = AppTheme.typography.button - ) - } - } - } -} - -@Preview -@Composable -private fun SignInHintPreview() { - AppTheme { - HomeNoHealthCardSignInHint({}) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt index c7c5dc04..7fde440b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt @@ -1,29 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") package de.gematik.ti.erp.app.prescription.ui -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -35,6 +36,7 @@ import androidx.compose.material.Icon import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.rounded.AddAPhoto import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Close @@ -42,110 +44,124 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.ui.ChooseAvatar -import de.gematik.ti.erp.app.profiles.ui.profileColor +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData.AvatarDimensions +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData.AvatarDimensions.Default +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData.AvatarDimensions.Small +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.ui.components.ChooseAvatar +import de.gematik.ti.erp.app.profiles.ui.components.color +import de.gematik.ti.erp.app.profiles.ui.components.profileColor +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.OutlinedIconButton import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.util.encoders.Base64 +import java.util.UUID +import kotlin.time.Duration.Companion.days @Composable -fun SmallMainScreenAvatar( - profile: ProfilesUseCaseData.Profile, - onClickAvatar: () -> Unit +fun ProfileConnectionSection( + activeProfile: UiState, + onClickAvatar: () -> Unit, + onClickLogin: () -> Unit, + onClickRefresh: () -> Unit ) { - val ssoTokenScope = profile.ssoTokenScope - - val currentSelectedColors = profileColor(profileColorNames = profile.color) + UiStateMachine(activeProfile) { profile -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically - Box( - modifier = Modifier, - contentAlignment = Alignment.Center - ) { - Surface( - modifier = Modifier.size(40.dp), - shape = CircleShape, - color = currentSelectedColors.backGroundColor ) { - Box( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClickAvatar), - contentAlignment = Alignment.Center - ) { - ChooseAvatar( - modifier = Modifier.size(16.dp), - emptyIcon = Icons.Rounded.AddAPhoto, - profile = profile, - avatar = profile.avatar + Column(modifier = Modifier.weight(0.6f), horizontalAlignment = Alignment.Start) { + MainScreenAvatar( + activeProfile = profile, + Small(), + onClickAvatar = onClickAvatar + ) + } + Column(modifier = Modifier.weight(0.4f), horizontalAlignment = Alignment.End) { + ConnectionHelper( + isProfileWithValidSsoTokenScope = profile.isSSOTokenValid(), + onClickLogin = onClickLogin, + onClickRefresh = onClickRefresh ) } } - if (profile.lastAuthenticated != null) { - Box( - modifier = Modifier - .size(20.dp) - .align(Alignment.BottomEnd) - .offset(8.dp, 6.dp) - .clip(CircleShape) - .aspectRatio(1f) - .border( - 2.dp, - AppTheme.colors.neutral000, - CircleShape - ) - .background( - color = if (ssoTokenScope?.token?.isValid() == true) { - AppTheme.colors.green200 - } else { - AppTheme.colors.neutral200 - } - ) - - ) { - when { - ssoTokenScope?.token?.isValid() == true -> { - Icon( - Icons.Rounded.Check, - null, - tint = AppTheme.colors.green600, - modifier = Modifier.size(12.dp).align(Alignment.Center) - ) - } + } +} - else -> { - Icon( - Icons.Rounded.Close, - null, - tint = AppTheme.colors.neutral600, - modifier = Modifier.size(12.dp).align(Alignment.Center) - ) +@Composable +fun MainScreenAvatar( + activeProfile: ProfilesUseCaseData.Profile, + avatarDimension: AvatarDimensions = Default(), + onClickAvatar: () -> Unit +) { + val isTokenValid = activeProfile.isSSOTokenValid() + val isRegistered = activeProfile.lastAuthenticated != null + Row(verticalAlignment = Alignment.CenterVertically) { + when (avatarDimension) { + is Small -> { + AvatarScreen( + activeProfile, + avatarDimension.dimension, + onClickAvatar + ) + if (isRegistered) { + SpacerMedium() + var fontColor = AppTheme.colors.neutral600 + var statusText = stringResource(R.string.not_logged_in) + if (isTokenValid) { + fontColor = AppTheme.colors.green800 + statusText = stringResource(R.string.logged_in) } + Text(modifier = Modifier.padding(end = PaddingDefaults.Medium), text = statusText, color = fontColor, style = AppTheme.typography.subtitle2) } } + + is Default -> AvatarScreen( + activeProfile, + avatarDimension.dimension, + onClickAvatar + ) } } } @Composable -fun MainScreenAvatar( +fun AvatarScreen( profile: ProfilesUseCaseData.Profile, + avatarDimension: PrescriptionScreenData.AvatarDimension, onClickAvatar: () -> Unit ) { - val currentSelectedColors = profileColor(profileColorNames = profile.color) - + val selectedColor = profileColor(profileColorNames = profile.color) + val isTokenValid = profile.isSSOTokenValid() Box( - modifier = Modifier, + modifier = Modifier.padding(), contentAlignment = Alignment.Center ) { Surface( - modifier = Modifier.size(96.dp), + modifier = Modifier.size(avatarDimension.avatarSize), shape = CircleShape, - color = currentSelectedColors.backGroundColor + color = selectedColor.backGroundColor ) { Box( modifier = Modifier @@ -154,9 +170,10 @@ fun MainScreenAvatar( contentAlignment = Alignment.Center ) { ChooseAvatar( - modifier = Modifier.size(24.dp), + modifier = Modifier.size(avatarDimension.chooseSize), emptyIcon = Icons.Rounded.AddAPhoto, - profile = profile, + image = profile.image, + profileColor = profile.color.color(), avatar = profile.avatar ) } @@ -164,73 +181,177 @@ fun MainScreenAvatar( if (profile.lastAuthenticated != null) { Box( modifier = Modifier - .size(40.dp) + .size(avatarDimension.statusSize) .align(Alignment.BottomEnd) - .offset(12.dp, 12.dp) + .offset(avatarDimension.statusOffset.x, avatarDimension.statusOffset.y) .clip(CircleShape) .aspectRatio(1f) + .background(if (isTokenValid) AppTheme.colors.green200 else AppTheme.colors.neutral200) .border( - 4.dp, + avatarDimension.statusBorder, AppTheme.colors.neutral000, CircleShape ) - ) { - when { - profile.ssoTokenScope?.token?.isValid() == true -> { - Image( - painterResource(R.drawable.main_screen_erx_icon_large), - null, - modifier = Modifier.align(Alignment.Center) - ) - } - - else -> { - Image( - painterResource(R.drawable.main_screen_erx_icon_gray_large), - null, - modifier = Modifier.align(Alignment.Center) - ) - } - } + Icon( + if (isTokenValid) Icons.Rounded.Check else Icons.Rounded.Close, + null, + tint = if (isTokenValid) AppTheme.colors.green500 else AppTheme.colors.neutral600, + modifier = Modifier + .size(avatarDimension.iconSize) + .align(Alignment.Center) + ) } } } } +@Requirement( + "A_24857#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Refreshing the prescription list happens only if the user is authenticated. " + + "If the user is not authenticated, the user is prompted to authenticate." +) @Composable -fun ProfileConnectionSection( - activeProfile: ProfilesUseCaseData.Profile, - onClickAvatar: () -> Unit, +fun ConnectionHelper( + isProfileWithValidSsoTokenScope: Boolean, + onClickLogin: () -> Unit, onClickRefresh: () -> Unit ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), - horizontalArrangement = Arrangement.SpaceBetween - ) { - SmallMainScreenAvatar( - profile = activeProfile, - onClickAvatar = onClickAvatar + if (isProfileWithValidSsoTokenScope) { + OutlinedIconButton( + onClick = onClickRefresh, + imageVector = Icons.Default.Replay, + contentDescription = stringResource(R.string.a11y_refresh) ) - ConnectionHelper( - profile = activeProfile, - onClickRefresh = onClickRefresh + } else { + TertiaryButton(onClickLogin) { + Text(stringResource(R.string.mainscreen_login)) + } + } +} + +@Preview +@Composable +fun ProfileConnectionSectionPreview() { + PreviewAppTheme { + ProfileConnectionSection( + activeProfile = UiState.Data( + ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = null, + ssoTokenScope = null, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) + ), + {}, + {}, + {} ) } } +@Preview @Composable -fun ConnectionHelper( - profile: ProfilesUseCaseData.Profile, - onClickRefresh: () -> Unit -) { - val ssoTokenScope = profile.ssoTokenScope +fun SimpleComposablePreview() { + PreviewAppTheme { + MainScreenAvatar( + activeProfile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = null, + ssoTokenScope = null, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ), + Small(), + {} + ) + } +} - if (ssoTokenScope?.token == null) { - TertiaryButton(onClickRefresh) { - Text(stringResource(R.string.mainscreen_login)) - } +@Preview +@Composable +fun SmallMainScreenAvatarPreview() { + val singleSignOnToken = IdpData.SingleSignOnToken( + token = UUID.randomUUID().toString(), + expiresOn = Clock.System.now().plus(200.days), + validOn = Clock.System.now().plus(20.days) + ) + val can = "123123" + val byteArray = Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) + val healthCertificate = X509CertificateHolder(byteArray) + + PreviewAppTheme { + MainScreenAvatar( + activeProfile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = IdpData.DefaultToken( + token = singleSignOnToken, + cardAccessNumber = can, + healthCardCertificate = healthCertificate + ), + avatar = ProfilesData.Avatar.ManWithPhone, + image = null + ), + Small(), + {} + ) + } +} + +@Preview +@Composable +fun MainScreenAvatarPreview() { + val singleSignOnToken = IdpData.SingleSignOnToken( + token = UUID.randomUUID().toString(), + expiresOn = Clock.System.now().plus(200.days), + validOn = Clock.System.now().plus(20.days) + ) + val can = "123123" + val byteArray = Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) + val healthCertificate = X509CertificateHolder(byteArray) + + PreviewAppTheme { + MainScreenAvatar( + activeProfile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = IdpData.DefaultToken( + token = singleSignOnToken, + cardAccessNumber = can, + healthCardCertificate = healthCertificate + ), + avatar = ProfilesData.Avatar.ManWithPhone, + image = null + ), + Default(), + {} + ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScanScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScanScreen.kt index aaa377bf..e1d6d0bd 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScanScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScanScreen.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui @@ -71,20 +71,13 @@ import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.IconToggleButton import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.CropFree -import androidx.compose.material.icons.rounded.ErrorOutline -import androidx.compose.material.icons.rounded.FlashOff -import androidx.compose.material.icons.rounded.FlashOn import androidx.compose.material.icons.rounded.SaveAlt import androidx.compose.material.icons.rounded.ShoppingBag import androidx.compose.material.rememberModalBottomSheetState @@ -110,8 +103,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -125,30 +116,36 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes import de.gematik.ti.erp.app.prescription.presentation.rememberScanPrescriptionController import de.gematik.ti.erp.app.prescription.ui.model.ScanData +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AlertDialog +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AccessToCameraDenied import de.gematik.ti.erp.app.utils.compose.BottomSheetAction -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.CameraTopBar +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringBold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.Locale import java.util.concurrent.Executors +// TODO: Cleanup to move more logic into the viewmodel to make it more testable @Requirement( "O.Purp_2#1", - "O.Data_6#1", sourceSpecification = "BSI-eRp-ePA", rationale = "Scanning tasks contains purpose related data input." ) @@ -159,25 +156,41 @@ class PrescriptionScanScreen( @OptIn(ExperimentalMaterialApi::class) @Composable override fun Content() { + val context = LocalContext.current + val dialog = LocalDialog.current + + val confirmCancelDialogEvent = ComposableEvent() + val scanPrescriptionController = rememberScanPrescriptionController() - val state by scanPrescriptionController.state + val scanPrescriptionState by scanPrescriptionController.state val overlayState by scanPrescriptionController.overlayState val vibrationPattern = scanPrescriptionController.vibration val scanner = scanPrescriptionController.scanner val processor = scanPrescriptionController.processor - val context = LocalContext.current - var camPermissionGranted by rememberSaveable { mutableStateOf(false) } + // stops the camera overlay from scanning data points + var stopCameraOverlayScan by remember { mutableStateOf(false) } - @Requirement( - "A_20193#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Request user permission for using the camera." - ) - val camPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { - camPermissionGranted = it + confirmCancelDialogEvent.listen { + dialog.show { + stopCameraOverlayScan = true + SaveDialog( + onDismissRequest = { + stopCameraOverlayScan = false + it.dismiss() + }, + onCancel = { + it.dismiss() + navController.navigate(PrescriptionRoutes.PrescriptionsScreen.path()) + } + ) } + } + + var camPermissionGranted by rememberSaveable { mutableStateOf(false) } + + val camPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { camPermissionGranted = it } + @Requirement( "O.Plat_3#2", sourceSpecification = "BSI-eRp-ePA", @@ -194,54 +207,52 @@ class PrescriptionScanScreen( var flashEnabled by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val coroutineScope = rememberCoroutineScope() - var cancelRequested by remember { mutableStateOf(false) } - BackHandler(!cancelRequested && state.hasCodesToSave()) { - cancelRequested = true - } - BackHandler(sheetState.isVisible) { - coroutineScope.launch { sheetState.hide() } - } - if (cancelRequested && state.hasCodesToSave()) { - SaveDialog( - onDismissRequest = { cancelRequested = false }, - onCancel = { navController.navigate(MainNavigationScreens.Prescriptions.path()) } - ) + // conditional back + val onBack: () -> Unit = { + if (sheetState.isVisible) { + coroutineScope.launch { sheetState.hide() } + } + if (scanPrescriptionState.hasCodesToSave()) { + confirmCancelDialogEvent.trigger() + } else { + navController.navigateAndClearStack(PrescriptionRoutes.PrescriptionsScreen.route) + } } + BackHandler { onBack() } + ModalBottomSheetLayout( sheetState = sheetState, sheetContent = { SheetContent( onClickSave = { scanPrescriptionController.saveToDatabase() - navController.navigate(MainNavigationScreens.Prescriptions.path()) + navController.navigate(PrescriptionRoutes.PrescriptionsScreen.path()) }, onClickRedeem = { scanPrescriptionController.saveToDatabase() - navController.navigate(MainNavigationScreens.Redeem.path()) + navController.navigate(RedeemRoutes.RedeemMethodSelection.path()) } ) } ) { if (camPermissionGranted) { PrescriptionScanScreenContent( - scanner, - processor, - flashEnabled, - onFlashToggled = { - flashEnabled = it - }, - onClickPrescription = { navController.navigate(MainNavigationScreens.Prescriptions.path()) }, - sheetState, - cancelRequested, - overlayState, - state, - coroutineScope + scanner = scanner, + processor = processor, + flashEnabled = flashEnabled, + sheetState = sheetState, + stopCameraOverlayScan = stopCameraOverlayScan, + overlayState = overlayState, + state = scanPrescriptionState, + coroutineScope = coroutineScope, + onFlashToggled = { flashEnabled = it }, + onClickClose = { onBack() } ) } else { - AccessDenied { - navController.navigate(MainNavigationScreens.Prescriptions.path()) + AccessToCameraDenied { + navController.navigate(PrescriptionRoutes.PrescriptionsScreen.path()) } } } @@ -257,35 +268,33 @@ private fun PrescriptionScanScreenContent( processor: TwoDCodeProcessor, flashEnabled: Boolean, onFlashToggled: (Boolean) -> Unit, - onClickPrescription: () -> Unit, + onClickClose: () -> Unit, sheetState: ModalBottomSheetState, - cancelRequested: Boolean, + stopCameraOverlayScan: Boolean, overlayState: ScanData.OverlayState, state: ScanData.State, coroutineScope: CoroutineScope ) { Box { CameraView( - scanner, - processor, - Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), + scanner = scanner, + processor = processor, flashEnabled = flashEnabled, onFlashToggled = onFlashToggled ) ScanOverlay( - enabled = !sheetState.isVisible && !cancelRequested, + modifier = Modifier.fillMaxSize(), + enabled = !sheetState.isVisible && !stopCameraOverlayScan, flashEnabled = flashEnabled, onFlashClick = onFlashToggled, - modifier = Modifier.fillMaxSize(), - onClickPrescription = onClickPrescription, + onClickClose = onClickClose, overlayState = overlayState ) if (state.snackBar.shouldShow()) { ActionBarButton( - state.snackBar, - onClick = { coroutineScope.launch { sheetState.show() } }, modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) @@ -295,7 +304,9 @@ private fun PrescriptionScanScreenContent( end = PaddingDefaults.Medium, top = PaddingDefaults.Medium, bottom = PaddingDefaults.XLarge - ) + ), + data = state.snackBar, + onClick = { coroutineScope.launch { sheetState.show() } } ) } } @@ -336,68 +347,12 @@ private fun SheetContent( Spacer(Modifier.navigationBarsPadding()) } -@Suppress("MagicNumber") -@Composable -private fun AccessDenied( - onClickPrescription: () -> Unit -) { - Surface( - color = Color.Black, - contentColor = Color.White, - modifier = Modifier.fillMaxSize() - ) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .testTag("camera/disallowed") - ) { - TopBar( - flashEnabled = false, - onClickPrescription = onClickPrescription, - onFlashClick = {} - ) - Spacer(Modifier.weight(0.4f)) - Column( - modifier = Modifier - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - Icons.Rounded.ErrorOutline, - null, - modifier = Modifier.size(48.dp) - ) - Text( - stringResource(R.string.cam_access_denied_headline), - style = AppTheme.typography.h6, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Text( - stringResource(R.string.cam_access_denied_description), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } - Spacer(Modifier.weight(0.6f)) - } - } -} - @Suppress("MagicNumber") @Composable private fun HapticAndAudibleFeedback(scanPrescriptionVibration: MutableSharedFlow) { val context = LocalContext.current - val toneGenerator = remember { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - ToneGenerator(AudioManager.STREAM_ACCESSIBILITY, 100) - } else { - ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100) - } - } + val toneGenerator = remember { ToneGenerator(AudioManager.STREAM_ACCESSIBILITY, 100) } LaunchedEffect(scanPrescriptionVibration) { scanPrescriptionVibration.collect { @@ -421,8 +376,17 @@ private fun HapticAndAudibleFeedback(scanPrescriptionVibration: MutableSharedFlo private fun SaveDialog( onDismissRequest: () -> Unit, onCancel: () -> Unit -) = - AlertDialog( +) { + ErezeptAlertDialog( + body = stringResource(R.string.cam_cancel_msg), + okText = stringResource(R.string.cam_cancel_ok), + dismissText = stringResource(R.string.cam_cancel_resume), + dismissButtonTestTag = "camera/saveDialog/dismissDialogButton", + confirmButtonTestTag = "camera/saveDialog/saveButton", + onConfirmRequest = onCancel, + onDismissRequest = onDismissRequest + ) + /* AlertDialog( onDismissRequest = onDismissRequest, text = { Text( @@ -440,11 +404,11 @@ private fun SaveDialog( Text(stringResource(R.string.cam_cancel_ok).uppercase(Locale.getDefault())) } } - ) + ) */ +} @Suppress("MagicNumber") private fun beep(toneGenerator: ToneGenerator, pattern: ScanData.VibrationPattern) { - @Suppress("NON_EXHAUSTIVE_WHEN") when (pattern) { ScanData.VibrationPattern.Saved -> toneGenerator.startTone( ToneGenerator.TONE_PROP_PROMPT, @@ -468,44 +432,14 @@ private fun vibrate(vibrator: Vibrator, pattern: ScanData.VibrationPattern) { val canVibrate: Boolean = vibrator.hasVibrator() if (canVibrate) { - if (VERSION.SDK_INT >= VERSION_CODES.O) { - vibrator.vibrate( - when (pattern) { - ScanData.VibrationPattern.Focused -> - createOneShot(100L, 100) - - ScanData.VibrationPattern.Saved -> - createOneShot(300L, 100) - - ScanData.VibrationPattern.Error -> - createWaveform( - longArrayOf(100, 100, 300), - intArrayOf( - 100, - 0, - 100 - ), - -1 - ) - - ScanData.VibrationPattern.None -> error("Should not be reached") - } - ) - } else { - @Suppress("DEPRECATION") + vibrator.vibrate( when (pattern) { - ScanData.VibrationPattern.Focused -> - vibrator.vibrate(longArrayOf(0L, 100L), -1) - - ScanData.VibrationPattern.Saved -> - vibrator.vibrate(longArrayOf(0L, 300L), -1) - - ScanData.VibrationPattern.Error -> - vibrator.vibrate(longArrayOf(0L, 100L, 100L, 300L), -1) - + ScanData.VibrationPattern.Focused -> createOneShot(100L, 100) + ScanData.VibrationPattern.Saved -> createOneShot(300L, 100) + ScanData.VibrationPattern.Error -> createWaveform(longArrayOf(100, 100, 300), intArrayOf(100, 0, 100), -1) ScanData.VibrationPattern.None -> error("Should not be reached") } - } + ) } } @@ -704,50 +638,6 @@ private fun CameraView( } } -@Composable -private fun TopBar( - flashEnabled: Boolean, - onClickPrescription: () -> Unit, - onFlashClick: (Boolean) -> Unit -) { - Surface( - color = Color.Unspecified, - contentColor = Color.White, - modifier = Modifier.fillMaxWidth() - ) { - Row(modifier = Modifier.padding(16.dp)) { - val accCancel = stringResource(R.string.cam_acc_cancel) - val accTorch = stringResource(R.string.cam_acc_torch) - - IconButton( - onClick = { onClickPrescription() }, - modifier = Modifier - .testTag("camera/closeButton") - .semantics { contentDescription = accCancel } - ) { - Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp)) - } - - Spacer(modifier = Modifier.weight(1f)) - - IconToggleButton( - checked = flashEnabled, - onCheckedChange = onFlashClick, - modifier = Modifier - .testTag("camera/flashToggle") - .semantics { contentDescription = accTorch } - ) { - val ic = if (flashEnabled) { - Icons.Rounded.FlashOn - } else { - Icons.Rounded.FlashOff - } - Icon(ic, null, modifier = Modifier.size(24.dp)) - } - } - } -} - @Suppress("ComplexMethod", "MagicNumber") @Composable private fun ScanOverlay( @@ -755,7 +645,7 @@ private fun ScanOverlay( flashEnabled: Boolean, onFlashClick: (Boolean) -> Unit, modifier: Modifier = Modifier, - onClickPrescription: () -> Unit, + onClickClose: () -> Unit, overlayState: ScanData.OverlayState ) { var points by remember { mutableStateOf(FloatArray(8)) } @@ -816,9 +706,9 @@ private fun ScanOverlay( .fillMaxSize() .systemBarsPadding() ) { - TopBar( + CameraTopBar( flashEnabled = flashEnabled, - onClickPrescription = onClickPrescription, + onClickClose = onClickClose, onFlashClick = onFlashClick ) Spacer(modifier = Modifier.size(24.dp)) @@ -834,6 +724,7 @@ private fun ScanOverlay( ) { val infTransition = rememberInfiniteTransition() val scale by infTransition.animateFloat( + label = "", initialValue = 1.0f, targetValue = 1.2f, animationSpec = infiniteRepeatable( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt deleted file mode 100644 index 8889379e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt +++ /dev/null @@ -1,791 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material.Card -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowRight -import androidx.compose.material.icons.rounded.ArrowDownward -import androidx.compose.material.icons.rounded.WarningAmber -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData.scannedPrescriptionIndex -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.presentation.PrescriptionsController -import de.gematik.ti.erp.app.prescription.ui.model.SentOrCompletedPhrase -import de.gematik.ti.erp.app.prescription.ui.model.sentOrCompleted -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription -import de.gematik.ti.erp.app.prescriptionId -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.ProfileConnectionState -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.connectionState -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.DynamicText -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.TertiaryButton -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import de.gematik.ti.erp.app.utils.compose.dateString -import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString -import de.gematik.ti.erp.app.utils.compose.timeString -import de.gematik.ti.erp.app.utils.toFormattedDate -import de.gematik.ti.erp.app.utils.toStartOfDayInUTC -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.format.DateTimeFormatter - -const val ZERO_DAYS_LEFT = 0L -const val ONE_DAY_LEFT = 1L -const val TWO_DAYS_LEFT = 2L - -@Composable -fun PrescriptionsScreen( - controller: PrescriptionsController, - mainScreenController: MainScreenController, // TODO: Hoist it out - activeProfile: ProfilesUseCaseData.Profile, - onShowCardWall: () -> Unit, - onElevateTopBar: (Boolean) -> Unit, - onClickPrescription: (String) -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit -) { - val activePrescriptions by controller.activePrescriptionsState - val archivedPrescriptions by controller.archivedPrescriptionsState - - PrescriptionScreen( - activeProfile = activeProfile, - activePrescriptions = activePrescriptions, - isArchiveEmpty = archivedPrescriptions.isEmpty(), - mainScreenController = mainScreenController, - onElevateTopBar = onElevateTopBar, - onClickPrescription = onClickPrescription, - onClickAvatar = onClickAvatar, - onClickArchive = onClickArchive, - onShowCardWall = onShowCardWall - ) -} - -@Composable -private fun PrescriptionScreen( - activeProfile: ProfilesUseCaseData.Profile, - activePrescriptions: List, - isArchiveEmpty: Boolean, - mainScreenController: MainScreenController, - onElevateTopBar: (Boolean) -> Unit, - onClickPrescription: (String) -> Unit, - onShowCardWall: () -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit -) { - var showUserNotAuthenticatedDialog by remember { mutableStateOf(false) } - - if (showUserNotAuthenticatedDialog) { - UserNotAuthenticatedDialog( - onCancel = { showUserNotAuthenticatedDialog = false }, - onShowCardWall = onShowCardWall - ) - } - - RefreshScaffold( - profileId = activeProfile.id, - onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, - mainScreenController = mainScreenController, - onShowCardWall = onShowCardWall - ) { onRefresh -> - Prescriptions( - activeProfile = activeProfile, - activePrescriptions = activePrescriptions, - isArchiveEmpty = isArchiveEmpty, - onClickRefresh = { onRefresh(true, MutatePriority.UserInput) }, - onClickAvatar = onClickAvatar, - onElevateTopBar = onElevateTopBar, - onClickArchive = onClickArchive, - onClickPrescription = onClickPrescription - ) - } -} - -@Requirement( - "A_21576#2", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Show biometric credentials changed dialog." -) -@Composable -fun UserNotAuthenticatedDialog(onCancel: () -> Unit, onShowCardWall: () -> Unit) { - CommonAlertDialog( - header = stringResource(R.string.user_not_authenticated_dialog_header), - info = stringResource(R.string.user_not_authenticated_dialog_info), - cancelText = stringResource(R.string.user_not_authenticated_dialog_cancel), - actionText = stringResource(R.string.user_not_authenticated_dialog_connect), - onCancel = onCancel - ) { - onShowCardWall() - } -} - -val CardPaddingModifier = Modifier - .padding( - bottom = PaddingDefaults.Medium, - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium - ) - .fillMaxWidth() - -@Composable -private fun Prescriptions( - activeProfile: ProfilesUseCaseData.Profile, - activePrescriptions: List, - isArchiveEmpty: Boolean, - onClickPrescription: (String) -> Unit, - onElevateTopBar: (Boolean) -> Unit, - onClickRefresh: () -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit -) { - PrescriptionsContent( - activeProfile = activeProfile, - activePrescriptions = activePrescriptions, - isArchiveEmpty = isArchiveEmpty, - onElevateTopBar = onElevateTopBar, - onClickPrescription = onClickPrescription, - onClickRefresh = onClickRefresh, - onClickAvatar = onClickAvatar, - onClickArchive = onClickArchive - ) -} - -private val FabPadding = 68.dp - -@Composable -private fun PrescriptionsContent( - activeProfile: ProfilesUseCaseData.Profile, - activePrescriptions: List, - isArchiveEmpty: Boolean, - onElevateTopBar: (Boolean) -> Unit, - onClickPrescription: (String) -> Unit, - onClickRefresh: () -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit -) { - val listState = rememberLazyListState() - - LaunchedEffect(Unit) { - snapshotFlow { - listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 - }.collect { - onElevateTopBar(it) - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TestTag.Prescriptions.Content), - state = listState, - contentPadding = PaddingValues(bottom = FabPadding), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Top - ) { - if (activePrescriptions.isNotEmpty()) { - item { - SpacerXXLarge() - ProfileConnectionSection( - activeProfile = activeProfile, - onClickAvatar = onClickAvatar, - onClickRefresh = onClickRefresh - ) - SpacerMedium() - } - prescriptionContent( - activePrescriptions = activePrescriptions, - onClickPrescription = onClickPrescription - ) - } else { - emptyContent( - activeProfile = activeProfile, - onClickConnect = onClickRefresh, - onClickAvatar = onClickAvatar - ) - } - if (!isArchiveEmpty) { - item { - SpacerLarge() - TextButton( - onClick = onClickArchive, - modifier = Modifier.testTag(TestTag.Prescriptions.ArchiveButton) - ) { - Text(stringResource(R.string.archived_prescriptions_button)) - } - SpacerLarge() - } - } - } -} - -fun LazyListScope.emptyContent( - activeProfile: ProfilesUseCaseData.Profile, - onClickConnect: () -> Unit, - onClickAvatar: () -> Unit -) { - item { - Spacer(modifier = Modifier.size(80.dp)) - MainScreenAvatar( - profile = activeProfile, - onClickAvatar = onClickAvatar - ) - } - if (activeProfile.connectionState() != ProfileConnectionState.LoggedIn) { - item { - SpacerMedium() - TertiaryButton(onClickConnect, modifier = Modifier.testTag(TestTag.Main.LoginButton)) { - Text(stringResource(R.string.mainscreen_login)) - } - } - item { - SpacerLarge() - Text( - stringResource(R.string.mainscreen_empty_content_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier.testTag(TestTag.Main.CenterScreenMessageField) - ) - } - item { - SpacerSmall() - Text( - stringResource(R.string.mainscreen_empty_not_connected_info), - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - style = AppTheme.typography.body2, - textAlign = TextAlign.Center - ) - } - } else { - item { - SpacerLarge() - Text( - stringResource(R.string.mainscreen_empty_content_header), - style = AppTheme.typography.subtitle1 - ) - } - item { - SpacerMedium() - Text( - stringResource(R.string.mainscreen_empty_connected_info), - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - style = AppTheme.typography.body2, - textAlign = TextAlign.Center - ) - SpacerMedium() - Icon(Icons.Rounded.ArrowDownward, null) - } - } -} - -private fun LazyListScope.prescriptionContent( - activePrescriptions: List, - onClickPrescription: (String) -> Unit -) { - val prescriptionsIndices = processPrescriptionsDayIndices(activePrescriptions) - - activePrescriptions.forEach { prescription -> - item(key = "prescription-${prescription.taskId}") { - when (prescription) { - is SyncedPrescription -> - FullDetailMedication( - prescription, - modifier = CardPaddingModifier, - onClick = { - onClickPrescription(prescription.taskId) - } - ) - - is ScannedPrescription -> { - LowDetailMedication( - modifier = CardPaddingModifier, - prescription, - onClick = { - scannedPrescriptionIndex = - prescriptionsIndices.getOrDefault(prescription.taskId, 1) - onClickPrescription(prescription.taskId) - } - ) - } - } - } - } -} - -private fun processPrescriptionsDayIndices(prescriptions: List): Map { - var previousPrescription: ScannedPrescription? = null - var dayIndex = 1 - val indexedPrescriptions = mutableMapOf() - - prescriptions.filterIsInstance().forEach { - val current = it.scannedOn.toFormattedDate() - val prev = previousPrescription?.scannedOn?.toFormattedDate() - if (current == prev) dayIndex++ else dayIndex = 1 - previousPrescription = it - indexedPrescriptions[it.taskId] = dayIndex - } - return indexedPrescriptions -} - -@Composable -fun readyPrescriptionStateInfo( - acceptDaysLeft: Long, - expiryDaysLeft: Long -): AnnotatedString? = when { - acceptDaysLeft == ZERO_DAYS_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append(stringResource(R.string.prescription_item_accept_only_today)) - } - - expiryDaysLeft == ZERO_DAYS_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append(stringResource(R.string.prescription_item_expiration_only_today)) - } - - acceptDaysLeft == ONE_DAY_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append(stringResource(R.string.prescription_item_accept_only_tomorrow)) - } - - expiryDaysLeft == ONE_DAY_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append( - stringResource(R.string.prescription_item_expiration_only_tomorrow) - ) - } - - acceptDaysLeft == TWO_DAYS_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append( - annotatedStringResource( - R.string.prescription_item_two_accept_days_left, - AnnotatedString(acceptDaysLeft.toString()) - ) - ) - } - - expiryDaysLeft == TWO_DAYS_LEFT -> buildAnnotatedString { - appendInlineContent( - id = "warningAmber", - alternateText = stringResource(R.string.prescription_item_warning_amber) - ) - append( - annotatedStringResource( - R.string.prescription_item_two_expiration_days_left, - AnnotatedString(expiryDaysLeft.toString()) - ) - ) - } - - acceptDaysLeft > TWO_DAYS_LEFT -> annotatedStringResource( - R.string.prescription_item_accept_days_left, - AnnotatedString((acceptDaysLeft).toString()) - ) - - expiryDaysLeft > TWO_DAYS_LEFT -> annotatedStringResource( - R.string.prescription_item_expiration_days_left, - AnnotatedString((expiryDaysLeft).toString()) - ) - - else -> null -} - -@Composable -fun PrescriptionStateInfo( - state: SyncedTaskData.SyncedTask.TaskState, - now: Instant = Clock.System.now(), - textAlign: TextAlign = TextAlign.Left -) { - val warningAmber = mapOf( - "warningAmber" to InlineTextContent( - Placeholder( - width = 0.em, - height = 0.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter - ) - ) { - Icon( - imageVector = Icons.Rounded.WarningAmber, - modifier = Modifier.padding(end = PaddingDefaults.Tiny), - contentDescription = null, - tint = AppTheme.colors.red600 - ) - } - ) - - when (state) { - is SyncedTaskData.SyncedTask.LaterRedeemable -> { - Text( - text = dateWithIntroductionString( - R.string.pres_detail_medication_redeemable_on, - state.redeemableOn - ), - style = AppTheme.typography.body2l, - textAlign = textAlign - ) - } - - is SyncedTaskData.SyncedTask.Ready -> { - val expiryDaysLeft = - remember { (state.expiresOn - now.toStartOfDayInUTC()).inWholeDays } - val acceptDaysLeft = - remember { (state.acceptUntil - now.toStartOfDayInUTC()).inWholeDays } - - val text = readyPrescriptionStateInfo(acceptDaysLeft, expiryDaysLeft) - - when { - acceptDaysLeft in ZERO_DAYS_LEFT..TWO_DAYS_LEFT || - expiryDaysLeft in ZERO_DAYS_LEFT..TWO_DAYS_LEFT -> - text?.let { - DynamicText( - it, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = AppTheme.typography.body2, - color = AppTheme.colors.red600, - inlineContent = warningAmber - ) - } - - acceptDaysLeft > TWO_DAYS_LEFT || expiryDaysLeft > TWO_DAYS_LEFT -> - text?.let { Text(it, style = AppTheme.typography.body2, textAlign = textAlign) } - - else -> {} - } - } - - is SyncedTaskData.SyncedTask.InProgress -> { - val text = sentOrCompletedPhrase(state.lastModified, now) - Text(text, style = AppTheme.typography.body2, textAlign = textAlign) - } - - is SyncedTaskData.SyncedTask.Pending -> { - val text = sentOrCompletedPhrase(state.sentOn, now) - Text(text, style = AppTheme.typography.body2, textAlign = textAlign) - } - - is SyncedTaskData.SyncedTask.Expired -> { - Text( - dateWithIntroductionString( - R.string.pres_detail_medication_expired_on, - state.expiredOn - ), - style = AppTheme.typography.body2, - textAlign = textAlign - ) - } - - is SyncedTaskData.SyncedTask.Other -> { - if (state.state == SyncedTaskData.TaskStatus.Completed) { - val text = sentOrCompletedPhrase(state.lastModified, now, true) - Text(text, style = AppTheme.typography.body2, textAlign = textAlign) - } - } - } -} - -@Composable -private fun sentOrCompletedPhrase( - lastModified: Instant, - now: Instant, - completed: Boolean = false -): String = - when ( - val phrase = - sentOrCompleted( - lastModified = lastModified, - now = now, - completed = completed - ) - ) { - SentOrCompletedPhrase.RedeemedJustNow -> stringResource(R.string.received_now) - SentOrCompletedPhrase.SentJustNow -> stringResource(R.string.sent_now) - - is SentOrCompletedPhrase.RedeemedMinutesAgo -> - annotatedStringResource( - R.string.received_x_min_ago, - phrase.minutes - ).toString() - - is SentOrCompletedPhrase.SentMinutesAgo -> - annotatedStringResource( - R.string.sent_x_min_ago, - phrase.minutes - ).toString() - - is SentOrCompletedPhrase.RedeemedHoursAgo -> - annotatedStringResource( - R.string.received_on_minute, - remember { timeString(lastModified.toLocalDateTime(TimeZone.currentSystemDefault())) } - ).toString() - - is SentOrCompletedPhrase.SentHoursAgo -> - annotatedStringResource( - R.string.sent_on_minute, - remember { timeString(lastModified.toLocalDateTime(TimeZone.currentSystemDefault())) } - ).toString() - - is SentOrCompletedPhrase.RedeemedOn -> - annotatedStringResource( - R.string.received_on_day, - remember { dateString(phrase.on.toLocalDateTime(TimeZone.currentSystemDefault())) } - ).toString() - - is SentOrCompletedPhrase.SentOn -> - annotatedStringResource( - R.string.sent_on_day, - remember { dateString(phrase.on.toLocalDateTime(TimeZone.currentSystemDefault())) } - ).toString() - } - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun FullDetailMedication( - prescription: SyncedPrescription, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - val showDirectAssignmentLabel by remember(prescription) { - derivedStateOf { - val isCompleted = - (prescription.state as? SyncedTaskData.SyncedTask.Other)?.state == SyncedTaskData.TaskStatus.Completed - - prescription.isDirectAssignment && !isCompleted - } - } - - Card( - modifier = modifier - .semantics { - prescriptionId = prescription.taskId - } - .testTag(TestTag.Prescriptions.FullDetailPrescription), - shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, color = AppTheme.colors.neutral300), - backgroundColor = AppTheme.colors.neutral050, - elevation = 0.dp, - onClick = onClick - ) { - Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { - Column(modifier = Modifier.weight(1f)) { - val medicationName = - prescription.name - ?: stringResource(R.string.prescription_medication_default_name) - - Text( - modifier = Modifier.testTag(TestTag.Prescriptions.FullDetailPrescriptionName), - text = medicationName, - style = AppTheme.typography.subtitle1, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - - if (!prescription.isDirectAssignment) { - PrescriptionStateInfo(prescription.state) - } - - SpacerSmall() - - Row { - if (prescription.isIncomplete) { - FailureStatusChip() - } else if (showDirectAssignmentLabel) { - DirectAssignmentStatusChip(prescription.redeemedOn != null) - } else { - when (prescription.state) { - is SyncedTaskData.SyncedTask.InProgress -> InProgressStatusChip() - is SyncedTaskData.SyncedTask.Pending -> PendingStatusChip() - is SyncedTaskData.SyncedTask.Ready -> ReadyStatusChip() - is SyncedTaskData.SyncedTask.Expired -> ExpiredStatusChip() - is SyncedTaskData.SyncedTask.LaterRedeemable -> LaterRedeemableStatusChip() - - is SyncedTaskData.SyncedTask.Other -> { - when (prescription.state.state) { - SyncedTaskData.TaskStatus.Completed -> CompletedStatusChip() - else -> UnknownStatusChip() - } - } - } - } - if (prescription.prescriptionChipInformation.isPartOfMultiplePrescription) { - prescription.prescriptionChipInformation.numerator?.let { numerator -> - prescription.prescriptionChipInformation.denominator?.let { denominator -> - SpacerSmall() - NumeratorChip(numerator, denominator) - } - } - } - } - } - - Icon( - Icons.Filled.KeyboardArrowRight, - null, - tint = AppTheme.colors.neutral400, - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterVertically) - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun LowDetailMedication( - modifier: Modifier = Modifier, - prescription: ScannedPrescription, - onClick: () -> Unit -) { - val dateFormatter = remember { DateTimeFormatter.ofPattern("dd.MM.yyyy") } - - val scannedOn = remember { - prescription.scannedOn.toLocalDateTime(TimeZone.currentSystemDefault()) - .toJavaLocalDateTime().format(dateFormatter) - } - - val redeemedOn = remember { - prescription.redeemedOn?.toLocalDateTime(TimeZone.currentSystemDefault()) - ?.toJavaLocalDateTime()?.format(dateFormatter) - } - - val dateText = if (redeemedOn != null) { - stringResource(R.string.prs_low_detail_redeemed_on, redeemedOn) - } else { - stringResource(R.string.prs_low_detail_scanned_on, scannedOn) - } - - Card( - modifier = modifier, - shape = RoundedCornerShape(16.dp), - border = BorderStroke(1.dp, color = AppTheme.colors.neutral300), - elevation = 0.dp, - backgroundColor = AppTheme.colors.neutral050, - onClick = onClick - ) { - Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { - Column( - modifier = Modifier - .weight(1f) - ) { - val titlePrepend = stringResource(R.string.pres_details_scanned_medication) - - val name = prescription.name ?: "$titlePrepend ${prescription.index}" - - Text( - name, - style = AppTheme.typography.subtitle1 - ) - SpacerTiny() - Text( - dateText, - style = AppTheme.typography.body2l - ) - SpacerSmall() - - Row { - if (prescription.communications.isNotEmpty()) { - SentStatusChip() - } - } - } - - Icon( - Icons.Filled.KeyboardArrowRight, - null, - tint = AppTheme.colors.neutral400, - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterVertically) - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt deleted file mode 100644 index aa5b0132..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.Requirement - -interface PrescriptionServiceState - -@Requirement( - "O.Source_3", - "O.Source_4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Error messages are localized using the `PrescriptionServiceErrorState " + - "Search for `PrescriptionServiceErrorState`or State.Error to see all instances." + - "Most errors are localized with static text. Logging is only active on debug builds." -) -interface PrescriptionServiceErrorState : PrescriptionServiceState - -@Stable -sealed interface GeneralErrorState : PrescriptionServiceErrorState { - data object NetworkNotAvailable : GeneralErrorState - class ServerCommunicationFailedWhileRefreshing(val code: Int) : GeneralErrorState - data object FatalTruststoreState : GeneralErrorState - data object NoneEnrolled : GeneralErrorState - data object UserNotAuthenticated : GeneralErrorState - data object RedirectUrlForExternalAuthenticationWrong : GeneralErrorState -} - -@Immutable -data class RefreshedState(val nrOfNewPrescriptions: Int) : PrescriptionServiceState diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt index 27fbf913..82eed7cc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("TooManyFunctions") @@ -30,8 +30,10 @@ import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.DoneAll import androidx.compose.material.icons.rounded.EventBusy import androidx.compose.material.icons.rounded.HourglassTop @@ -53,8 +55,10 @@ import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable fun StatusChip( @@ -93,7 +97,9 @@ fun StatusChip( StatusChip( text = text, icon = icon?.let { - { Icon(it, tint = iconColor, contentDescription = null) } + { + Icon(it, tint = iconColor, contentDescription = null) + } }, textColor = textColor, backgroundColor = backgroundColor, @@ -181,6 +187,28 @@ fun CompletedStatusChip() = modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionRedeemed) ) +@Composable +fun ProvidedStatusChip() = + StatusChip( + text = stringResource(R.string.provided), + icon = Icons.Rounded.DoneAll, + textColor = AppTheme.colors.neutral600, + backgroundColor = AppTheme.colors.neutral200, + iconColor = AppTheme.colors.neutral500, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionProvided) + ) + +@Composable +fun DeletedStatusChip() = + StatusChip( + text = stringResource(R.string.prescription_status_deleted), + icon = Icons.Outlined.Delete, + textColor = AppTheme.colors.neutral600, + backgroundColor = AppTheme.colors.neutral200, + iconColor = AppTheme.colors.neutral500, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionDeleted) + ) + @Composable fun ExpiredStatusChip() = StatusChip( @@ -262,24 +290,7 @@ fun DirectAssignmentChip( ) @Composable -fun ScannedChip( - modifier: Modifier = Modifier, - onClick: () -> Unit -) = - StatusChip( - modifier = modifier - .clickable(role = Role.Button) { - onClick() - }, - text = stringResource(R.string.prescription_detail_scanned_chip), - icon = Icons.Outlined.Info, - textColor = AppTheme.colors.primary900, - backgroundColor = AppTheme.colors.primary100, - iconColor = AppTheme.colors.primary600 - ) - -@Composable -fun SubstitutionAllowedChip( +fun SubstitutionNotAllowedChip( modifier: Modifier = Modifier, onClick: () -> Unit ) = @@ -288,7 +299,7 @@ fun SubstitutionAllowedChip( .clickable(role = Role.Button) { onClick() }, - text = stringResource(R.string.prescription_detail_aut_idem_chip), + text = stringResource(R.string.prescription_details_substitution_not_allowed), icon = Icons.Outlined.Info, textColor = AppTheme.colors.primary900, backgroundColor = AppTheme.colors.primary100, @@ -311,3 +322,19 @@ fun FailureDetailsStatusChip( backgroundColor = AppTheme.colors.red100, iconColor = AppTheme.colors.red500 ) + +@LightDarkPreview +@Composable +fun SubstitutionNotallowedStatusChipPreview() { + PreviewAppTheme { + SubstitutionNotAllowedChip {} + } +} + +@LightDarkPreview +@Composable +fun PendingStatusChipPreview() { + PreviewAppTheme { + PendingStatusChip() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt index 1f99e554..d6d8c08a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.prescription.ui import android.graphics.Matrix diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt index e343b609..296d3edf 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui @@ -21,9 +21,9 @@ package de.gematik.ti.erp.app.prescription.ui import android.content.Context import android.util.Size import androidx.annotation.OptIn +import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import androidx.camera.core.ExperimentalGetImage import com.google.mlkit.common.MlKit import com.google.mlkit.common.sdkinternal.MlKitContext import com.google.mlkit.vision.barcode.BarcodeScanner @@ -32,9 +32,9 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import de.gematik.ti.erp.app.Requirement +import io.github.aakira.napier.Napier import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow -import io.github.aakira.napier.Napier private const val DEFAULT_SCAN_TIME = 250L @@ -44,15 +44,9 @@ private const val DEFAULT_SCAN_TIME = 250L rationale = "analyse the camera input" ) @Requirement( - "O.Data_8", + "O.Data_9#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "This controller uses the camera as an input device. Frames are processed but never " + - "stored, metadata is never created here." -) -@Requirement( - "O.Data_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The device camera is exclusively used for scanning data matrix codes, no picture files are created." + rationale = "The device camera is also used for scanning data matrix codes." ) class TwoDCodeScanner( @@ -72,6 +66,12 @@ class TwoDCodeScanner( ) private set + @Requirement( + "O.Data_8#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "This controller uses the camera as an input device. Frames are processed but never " + + "stored, metadata is never created here." + ) private val scanner: BarcodeScanner by lazy { BarcodeScanning.getClient( BarcodeScannerOptions.Builder() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt index 129b1819..47bb7fa3 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt @@ -1,26 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui import de.gematik.ti.erp.app.Requirement import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import io.github.aakira.napier.Napier import kotlinx.datetime.Instant diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ArchiveSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ArchiveSection.kt new file mode 100644 index 00000000..a6078db4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ArchiveSection.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.SpacerLarge + +fun LazyListScope.archiveSection( + isArchiveEmpty: Boolean, + onClickArchive: () -> Unit +) { + if (!isArchiveEmpty) { + item { + SpacerLarge() + TextButton( + onClick = onClickArchive, + modifier = Modifier.testTag(TestTag.Prescriptions.ArchiveButton) + ) { + Text(stringResource(R.string.archived_prescriptions_button)) + } + SpacerLarge() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CardPaddingModifier.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CardPaddingModifier.kt new file mode 100644 index 00000000..87053dd8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CardPaddingModifier.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +val CardPaddingModifier = + Modifier + .padding( + bottom = PaddingDefaults.Medium, + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + .fillMaxWidth() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CommonDrawerScreenContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CommonDrawerScreenContent.kt new file mode 100644 index 00000000..a89931ec --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/CommonDrawerScreenContent.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("ModifierParameter") +@Composable +internal fun CommonDrawerScreenContent( + modifierPrimary: Modifier = Modifier, + modifierText: Modifier = Modifier, + header: String, + info: String, + image: Painter, + connectButtonText: String, + cancelButtonText: String, + onClickConnect: () -> Unit, + onCancel: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SpacerSmall() + Image( + painter = image, + contentDescription = null + ) + Text( + text = header, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + text = info, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + SpacerLarge() + PrimaryButton( + modifier = modifierPrimary, + onClick = onClickConnect, + colors = ButtonDefaults.buttonColors( + contentColor = AppTheme.colors.primary600 + ), + contentPadding = PaddingValues( + vertical = SizeDefaults.oneHalf, + horizontal = SizeDefaults.sixfold + ) + ) { + Text( + text = connectButtonText, + style = AppTheme.typography.body2, + color = AppTheme.colors.neutral050 + ) + } + SpacerMedium() + TextButton( + onClick = onCancel, + modifier = modifierText, + contentPadding = PaddingValues(vertical = SizeDefaults.oneHalf) + ) { + Text(cancelButtonText) + } + } +} + +@LightDarkPreview +@Composable +internal fun CommonDrawerScreenContentPreview() { + PreviewAppTheme { + CommonDrawerScreenContent( + header = "Header", + info = "Info", + image = painterResource(R.drawable.man_phone_blue_circle), + connectButtonText = "Connect", + cancelButtonText = "Cancel", + onClickConnect = {}, + onCancel = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/EmptyContentSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/EmptyContentSection.kt new file mode 100644 index 00000000..aeb2ed47 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/EmptyContentSection.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDownward +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.ui.MainScreenAvatar +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.ProfileConnectionState +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.connectionState +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.uistate.UiState + +fun LazyListScope.emptyContentSection( + activeProfile: UiState, + onClickConnect: () -> Unit, + onClickAvatar: () -> Unit +) { + item { + Spacer(modifier = Modifier.size(SizeDefaults.tenfold)) + UiStateMachine(activeProfile) { profile -> + MainScreenAvatar( + activeProfile = profile, + onClickAvatar = onClickAvatar + ) + } + } + if (activeProfile.data?.connectionState() != ProfileConnectionState.LoggedIn) { + item { + SpacerMedium() + TertiaryButton(onClickConnect, modifier = Modifier.testTag(TestTag.Main.LoginButton)) { + Text(stringResource(R.string.mainscreen_login)) + } + } + item { + SpacerLarge() + Text( + stringResource(R.string.mainscreen_empty_content_header), + style = AppTheme.typography.subtitle1, + modifier = Modifier.testTag(TestTag.Main.CenterScreenMessageField) + ) + } + item { + SpacerSmall() + Text( + stringResource(R.string.mainscreen_empty_not_connected_info), + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.body2, + textAlign = TextAlign.Center + ) + } + } else { + item { + SpacerLarge() + Text( + stringResource(R.string.mainscreen_empty_content_header), + style = AppTheme.typography.subtitle1 + ) + } + item { + SpacerMedium() + Text( + stringResource(R.string.mainscreen_empty_connected_info), + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.body2, + textAlign = TextAlign.Center + ) + SpacerMedium() + Icon(Icons.Rounded.ArrowDownward, null) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/FullDetailMedication.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/FullDetailMedication.kt new file mode 100644 index 00000000..46279537 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/FullDetailMedication.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.ui.CompletedStatusChip +import de.gematik.ti.erp.app.prescription.ui.DeletedStatusChip +import de.gematik.ti.erp.app.prescription.ui.DirectAssignmentStatusChip +import de.gematik.ti.erp.app.prescription.ui.ExpiredStatusChip +import de.gematik.ti.erp.app.prescription.ui.FailureStatusChip +import de.gematik.ti.erp.app.prescription.ui.InProgressStatusChip +import de.gematik.ti.erp.app.prescription.ui.LaterRedeemableStatusChip +import de.gematik.ti.erp.app.prescription.ui.NumeratorChip +import de.gematik.ti.erp.app.prescription.ui.PendingStatusChip +import de.gematik.ti.erp.app.prescription.ui.ProvidedStatusChip +import de.gematik.ti.erp.app.prescription.ui.ReadyStatusChip +import de.gematik.ti.erp.app.prescription.ui.UnknownStatusChip +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription +import de.gematik.ti.erp.app.prescriptionId +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.Instant +import kotlinx.datetime.plus + +@Suppress("CyclomaticComplexMethod") +@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class) +@Composable +fun FullDetailMedication( + prescription: SyncedPrescription, + modifier: Modifier = Modifier, + now: Instant = Clock.System.now(), + onClick: () -> Unit +) { + val showDirectAssignmentLabel by remember(prescription) { + derivedStateOf { + val isCompleted = + (prescription.state as? SyncedTaskData.SyncedTask.Other)?.state == SyncedTaskData.TaskStatus.Completed + + prescription.isDirectAssignment && !isCompleted + } + } + + Card( + modifier = + modifier + .semantics { + prescriptionId = prescription.taskId + } + .testTag(TestTag.Prescriptions.FullDetailPrescription), + shape = RoundedCornerShape(SizeDefaults.double), + border = BorderStroke(SizeDefaults.eighth, color = AppTheme.colors.neutral300), + backgroundColor = AppTheme.colors.neutral050, + elevation = SizeDefaults.zero, + onClick = onClick + ) { + val textColor = AppTheme.colors.neutral800 + Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { + Column(modifier = Modifier.weight(1f)) { + val medicationName = + prescription.name + ?: stringResource(R.string.prescription_medication_default_name) + + Text( + modifier = Modifier.testTag(TestTag.Prescriptions.FullDetailPrescriptionName), + text = medicationName, + color = textColor, + style = AppTheme.typography.subtitle1, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + + SpacerTiny() + + if (!prescription.isDirectAssignment) { + PrescriptionStateInfo( + state = prescription.state, + now = now + ) + } + + SpacerSmall() + + FlowRow( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(SizeDefaults.onefold) + ) { + if (prescription.isIncomplete) { + FailureStatusChip() + } else if (showDirectAssignmentLabel) { + DirectAssignmentStatusChip(prescription.redeemedOn != null) + } else { + when (prescription.state) { + is SyncedTaskData.SyncedTask.InProgress -> InProgressStatusChip() + is SyncedTaskData.SyncedTask.Pending -> PendingStatusChip() + is SyncedTaskData.SyncedTask.Ready -> ReadyStatusChip() + is SyncedTaskData.SyncedTask.Expired -> ExpiredStatusChip() + is SyncedTaskData.SyncedTask.LaterRedeemable -> LaterRedeemableStatusChip() + + is SyncedTaskData.SyncedTask.Other -> { + when (prescription.state.state) { + SyncedTaskData.TaskStatus.Completed -> CompletedStatusChip() + else -> UnknownStatusChip() + } + } + + is SyncedTaskData.SyncedTask.Deleted -> DeletedStatusChip() + is SyncedTaskData.SyncedTask.Provided -> ProvidedStatusChip() + } + } + if (prescription.prescriptionChipInformation.isPartOfMultiplePrescription) { + prescription.prescriptionChipInformation.numerator?.let { numerator -> + prescription.prescriptionChipInformation.denominator?.let { denominator -> + SpacerSmall() + NumeratorChip(numerator, denominator) + } + } + } + if (prescription.prescriptionChipInformation.isSelfPayPrescription) { + SpacerSmall() + SelfPayerPrescriptionChip() + } + } + } + + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + null, + tint = AppTheme.colors.neutral400, + modifier = + Modifier + .size(SizeDefaults.triple) + .align(Alignment.CenterVertically) + ) + } + } +} + +@Suppress("FunctionNaming") +@LightDarkPreview +@Composable +fun FullDetailMedicationPreview() { + val now = Clock.System.now() + val later = now.plus(2, DateTimeUnit.HOUR) + PreviewAppTheme { + FullDetailMedication( + prescription = + SyncedPrescription( + taskId = "1", + name = "Ibuprofen", + state = + SyncedTaskData.SyncedTask.Ready( + expiresOn = Instant.DISTANT_FUTURE, + acceptUntil = Instant.DISTANT_FUTURE + ), + isDirectAssignment = false, + isIncomplete = false, + acceptUntil = later, + authoredOn = now, + expiresOn = later, + redeemedOn = null, + organization = "Organization", + prescriptionChipInformation = + Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = true, + numerator = "1", + denominator = "2" + ) + ) + ) { } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/LowDetailMedication.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/LowDetailMedication.kt new file mode 100644 index 00000000..5d3ff77b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/LowDetailMedication.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.ui.SentStatusChip +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun LowDetailMedication( + modifier: Modifier = Modifier, + prescription: ScannedPrescription, + onClick: () -> Unit +) { + val dateFormatter = remember { DateTimeFormatter.ofPattern("dd.MM.yyyy") } + + val scannedOn = + remember { + prescription.scannedOn + .toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime() + .format(dateFormatter) + } + + val redeemedOn = + remember { + prescription.redeemedOn + ?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.toJavaLocalDateTime() + ?.format(dateFormatter) + } + + val dateText = + if (redeemedOn != null) { + stringResource(R.string.prs_low_detail_redeemed_on, redeemedOn) + } else { + stringResource(R.string.prs_low_detail_scanned_on, scannedOn) + } + + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + border = BorderStroke(1.dp, color = AppTheme.colors.neutral300), + elevation = 0.dp, + backgroundColor = AppTheme.colors.neutral050, + onClick = onClick + ) { + Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { + Column( + modifier = + Modifier + .weight(1f) + ) { + Text( + prescription.name, + style = AppTheme.typography.subtitle1 + ) + SpacerTiny() + Text( + dateText, + style = AppTheme.typography.body2l + ) + SpacerSmall() + + Row { + if (prescription.communications.isNotEmpty()) { + SentStatusChip() + } + } + } + + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + null, + tint = AppTheme.colors.neutral400, + modifier = + Modifier + .size(24.dp) + .align(Alignment.CenterVertically) + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionContentSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionContentSection.kt new file mode 100644 index 00000000..1af328ea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionContentSection.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription + +fun LazyListScope.prescriptionContentSection( + activePrescriptions: List, + onClickPrescription: (String) -> Unit +) { + activePrescriptions.forEach { prescription -> + item(key = "prescription-${prescription.taskId}") { + when (prescription) { + is SyncedPrescription -> + FullDetailMedication( + modifier = CardPaddingModifier, + prescription = prescription, + onClick = { + onClickPrescription(prescription.taskId) + } + ) + + is ScannedPrescription -> { + LowDetailMedication( + modifier = CardPaddingModifier, + prescription, + onClick = { + onClickPrescription(prescription.taskId) + } + ) + } + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionStateInfo.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionStateInfo.kt new file mode 100644 index 00000000..622f7869 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionStateInfo.kt @@ -0,0 +1,390 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.WarningAmber +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.em +import de.gematik.ti.erp.app.core.LocalTimeZone +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.ui.model.SentOrCompletedPhrase +import de.gematik.ti.erp.app.prescription.ui.model.sentOrCompleted +import de.gematik.ti.erp.app.prescription.ui.preview.prescriptionStatePreviews +import de.gematik.ti.erp.app.prescription.ui.screen.ONE_DAY_LEFT +import de.gematik.ti.erp.app.prescription.ui.screen.TWO_DAYS_LEFT +import de.gematik.ti.erp.app.prescription.ui.screen.ZERO_DAYS_LEFT +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.DynamicText +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.dateString +import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.timeString +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.toLocalDateTime + +// todo: split into smaller functions ready/laterRedeemable/inProgress/pending/deleted/expired/other +@Suppress("CyclomaticComplexMethod") +@Composable +fun PrescriptionStateInfo( + state: SyncedTaskData.SyncedTask.TaskState, + now: Instant = Clock.System.now(), + textColor: Color = AppTheme.colors.neutral800, + textAlign: TextAlign = TextAlign.Left +) { + val warningAmber = + mapOf( + "warningAmber" to + InlineTextContent( + Placeholder( + width = 0.em, + height = 0.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + ) { + Icon( + imageVector = Icons.Rounded.WarningAmber, + modifier = Modifier.padding(end = PaddingDefaults.Tiny), + contentDescription = null, + tint = AppTheme.colors.red600 + ) + } + ) + + when (state) { + is SyncedTaskData.SyncedTask.LaterRedeemable -> { + Text( + text = + dateWithIntroductionString( + R.string.pres_detail_medication_redeemable_on, + state.redeemableOn + ), + color = textColor, + style = AppTheme.typography.body2l, + textAlign = textAlign + ) + } + + is SyncedTaskData.SyncedTask.Ready -> { + val acceptDaysLeft = state.acceptDaysLeft(now) + val expiryDaysLeft = state.expiryDaysLeft(now) + val text = readyPrescriptionStateInfo(acceptDaysLeft, expiryDaysLeft) + + when { + acceptDaysLeft in ZERO_DAYS_LEFT..TWO_DAYS_LEFT || + expiryDaysLeft in ZERO_DAYS_LEFT..TWO_DAYS_LEFT -> + text?.let { + DynamicText( + it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = AppTheme.typography.body2, + color = AppTheme.colors.red600, + inlineContent = warningAmber, + textAlign = textAlign + ) + } + + acceptDaysLeft > TWO_DAYS_LEFT || expiryDaysLeft > TWO_DAYS_LEFT -> + text?.let { Text(it, style = AppTheme.typography.body2, color = textColor, textAlign = textAlign) } + + else -> {} + } + } + + is SyncedTaskData.SyncedTask.Provided -> { + val text = sentOrCompletedPhrase(state.lastMedicationDispense, now, state = state) + Text(text, style = AppTheme.typography.body2, color = textColor, textAlign = textAlign) + } + + is SyncedTaskData.SyncedTask.InProgress -> { + val text = sentOrCompletedPhrase(state.lastModified, now, state = state) + Text(text, style = AppTheme.typography.body2, color = textColor, textAlign = textAlign) + } + + is SyncedTaskData.SyncedTask.Pending -> { + val text = sentOrCompletedPhrase(state.sentOn, now, state = state) + Text(text, style = AppTheme.typography.body2, color = textColor, textAlign = textAlign) + } + + is SyncedTaskData.SyncedTask.Deleted -> { + Text( + dateWithIntroductionString( + R.string.pres_detail_medication_deleted, + state.lastModified + ), + style = AppTheme.typography.body2, + color = textColor, + textAlign = textAlign + ) + } + + is SyncedTaskData.SyncedTask.Expired -> { + Text( + dateWithIntroductionString( + R.string.pres_detail_medication_expired_on, + state.expiredOn + ), + style = AppTheme.typography.body2, + color = textColor, + textAlign = textAlign + ) + } + + is SyncedTaskData.SyncedTask.Other -> { + if (state.state == SyncedTaskData.TaskStatus.Completed) { + val text = sentOrCompletedPhrase(state.lastModified, now, true, state = state) + Text(text, style = AppTheme.typography.body2, color = textColor, textAlign = textAlign) + } + } + } +} + +@Composable +fun readyPrescriptionStateInfo( + acceptDaysLeft: Int, + expiryDaysLeft: Int +): AnnotatedString? = + when { + acceptDaysLeft == ZERO_DAYS_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append(stringResource(R.string.prescription_item_accept_only_today)) + } + + expiryDaysLeft == ZERO_DAYS_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append(stringResource(R.string.prescription_item_expiration_only_today)) + } + + acceptDaysLeft == ONE_DAY_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append(stringResource(R.string.prescription_item_accept_only_tomorrow)) + } + + expiryDaysLeft == ONE_DAY_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append( + stringResource(R.string.prescription_item_expiration_only_tomorrow) + ) + } + + acceptDaysLeft == TWO_DAYS_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append( + annotatedStringResource( + R.string.prescription_item_two_accept_days_left, + AnnotatedString(acceptDaysLeft.toString()) + ) + ) + } + + expiryDaysLeft == TWO_DAYS_LEFT -> + buildAnnotatedString { + appendInlineContent( + id = "warningAmber", + alternateText = stringResource(R.string.prescription_item_warning_amber) + ) + append( + annotatedStringResource( + R.string.prescription_item_two_expiration_days_left, + AnnotatedString(expiryDaysLeft.toString()) + ) + ) + } + + acceptDaysLeft > TWO_DAYS_LEFT -> + annotatedStringResource( + R.string.prescription_item_accept_days_left, + AnnotatedString((acceptDaysLeft).toString()) + ) + + expiryDaysLeft > TWO_DAYS_LEFT -> + annotatedStringResource( + R.string.prescription_item_expiration_days_left, + AnnotatedString((expiryDaysLeft).toString()) + ) + + else -> null + } + +@Composable +private fun sentOrCompletedPhrase( + lastModified: Instant, + now: Instant, + completed: Boolean = false, + state: SyncedTaskData.SyncedTask.TaskState +): String { + val timeZone = LocalTimeZone.current + return when ( + val phrase = sentOrCompleted( + lastModified = lastModified, + now = now, + completed = completed, + provided = state is SyncedTaskData.SyncedTask.Provided + ) + ) { + SentOrCompletedPhrase.RedeemedJustNow -> stringResource(R.string.received_now) + SentOrCompletedPhrase.SentJustNow -> { + if (state is SyncedTaskData.SyncedTask.Pending) { + stringResource(R.string.sent_now) + } else { + stringResource(R.string.accept_now) + } + } + + is SentOrCompletedPhrase.ProvidedHoursAgo -> { + annotatedStringResource( + R.string.provided_on_hour, + remember { timeString(lastModified.toLocalDateTime(timeZone)) } + ).toString() + } + SentOrCompletedPhrase.ProvidedJustNow -> stringResource(R.string.provided_now) + is SentOrCompletedPhrase.ProvidedMinutesAgo -> + annotatedStringResource(R.string.provided_minutes_ago, phrase.minutes).toString() + + is SentOrCompletedPhrase.ProvidedOn -> annotatedStringResource( + R.string.provided_on_date, + remember { dateString(phrase.on.toLocalDateTime(timeZone)) } + ).toString() + + is SentOrCompletedPhrase.RedeemedMinutesAgo -> { + annotatedStringResource(R.string.received_x_min_ago, phrase.minutes).toString() + } + + is SentOrCompletedPhrase.SentMinutesAgo -> { + val resourceId = + if (state is SyncedTaskData.SyncedTask.Pending) { + R.string.sent_x_min_ago + } else { + R.string.accept_x_min_ago + } + annotatedStringResource(resourceId, phrase.minutes).toString() + } + + is SentOrCompletedPhrase.RedeemedHoursAgo -> { + annotatedStringResource( + R.string.received_on_minute, + remember { timeString(lastModified.toLocalDateTime(timeZone)) } + ).toString() + } + + is SentOrCompletedPhrase.SentHoursAgo -> { + val resourceId = + if (state is SyncedTaskData.SyncedTask.Pending) { + R.string.sent_on_minute + } else { + R.string.accept_on_minute + } + annotatedStringResource( + resourceId, + remember { timeString(lastModified.toLocalDateTime(timeZone)) } + ).toString() + } + + is SentOrCompletedPhrase.RedeemedOn -> { + annotatedStringResource( + R.string.received_on_day, + remember { dateString(phrase.on.toLocalDateTime(timeZone)) } + ).toString() + } + + is SentOrCompletedPhrase.SentOn -> { + val resourceId = + if (state is SyncedTaskData.SyncedTask.Pending) { + R.string.sent_on_day + } else { + R.string.accept_on_day + } + annotatedStringResource( + resourceId, + remember { dateString(phrase.on.toLocalDateTime(timeZone)) } + ).toString() + } + } +} + +@Preview +@Composable +fun PrescriptionStateInfosCombinedPreview() { + PreviewAppTheme { + Column { + prescriptionStatePreviews.forEach { previewData -> + Text( + previewData.name, + style = AppTheme.typography.caption2, + color = AppTheme.colors.neutral800, + textAlign = TextAlign.Start + ) + SpacerTiny() + PrescriptionStateInfo( + state = previewData.prescriptionState, + now = previewData.now, + textAlign = TextAlign.Center + ) + SpacerTiny() + Divider() + SpacerMedium() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionsLoadingShimmer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionsLoadingShimmer.kt new file mode 100644 index 00000000..fbf35db0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/PrescriptionsLoadingShimmer.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.CircularShapeShimmer +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.LimitedTextShimmer +import de.gematik.ti.erp.app.utils.compose.RowTextShimmer +import de.gematik.ti.erp.app.utils.compose.SquareShapeShimmer +import de.gematik.ti.erp.app.utils.compose.StatusChipShimmer +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +// TODO: To be used when the user does pull to refresh +@Composable +internal fun PrescriptionScreenShimmer() { + Column { + SpacerSmall() + CircularShapeShimmer( + modifier = Modifier.padding(start = PaddingDefaults.Medium) + ) + SpacerSmall() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + PrescriptionCardShimmer() + } +} + +@Composable +private fun PrescriptionCardShimmer() { + Card( + modifier = Modifier + .padding(bottom = PaddingDefaults.Medium) + .padding(horizontal = PaddingDefaults.Medium), + shape = RoundedCornerShape(SizeDefaults.double), + border = BorderStroke( + SizeDefaults.eighth, + color = AppTheme.colors.neutral300 + ), + elevation = SizeDefaults.zero, + backgroundColor = AppTheme.colors.neutral050 + ) { + Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { + Column( + modifier = Modifier + .weight(1f) + ) { + LimitedTextShimmer() + SpacerTiny() + RowTextShimmer( + modifier = Modifier.padding(end = PaddingDefaults.Medium) + ) + SpacerSmall() + + StatusChipShimmer() + } + + SquareShapeShimmer( + modifier = Modifier.align(Alignment.CenterVertically), + size = SizeDefaults.double + ) + } + } +} + +@LightDarkPreview +@Composable +fun PrescriptionScreenShimmerPreview() { + PreviewAppTheme { + PrescriptionScreenShimmer() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ProfileConnectorSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ProfileConnectorSection.kt new file mode 100644 index 00000000..e02b3d67 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/ProfileConnectorSection.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import de.gematik.ti.erp.app.prescription.ui.ProfileConnectionSection +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXLarge +import de.gematik.ti.erp.app.utils.uistate.UiState + +fun LazyListScope.profileConnectorSection( + activeProfile: UiState, + onClickAvatar: () -> Unit, + onClickLogin: () -> Unit, + onClickRefresh: () -> Unit +) { + item { + SpacerXLarge() + ProfileConnectionSection( + activeProfile = activeProfile, + onClickAvatar = onClickAvatar, + onClickLogin = onClickLogin, + onClickRefresh = onClickRefresh + ) + SpacerMedium() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/SelfPayerPrescriptionChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/SelfPayerPrescriptionChip.kt new file mode 100644 index 00000000..cc9a278b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/SelfPayerPrescriptionChip.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.ui.StatusChip +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview + +@Composable +fun SelfPayerPrescriptionChip() = + StatusChip( + text = stringResource(R.string.prescription_status_self_pay), + textColor = AppTheme.colors.neutral600, + backgroundColor = AppTheme.colors.neutral100 + ) + +@LightDarkPreview +@Composable +fun PreviewSelfPayPrescriptionChip() { + SelfPayerPrescriptionChip() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/UserNotAuthenticatedDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/UserNotAuthenticatedDialog.kt new file mode 100644 index 00000000..9e9edeb8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/components/UserNotAuthenticatedDialog.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun UserNotAuthenticatedDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onShowCardWall: () -> Unit +) { + event.listen { + dialogScaffold.show { + UserNotAuthenticatedDialog( + onShowCardWall = { + onShowCardWall() + it.dismiss() + }, + onCancel = { it.dismiss() } + ) + } + } +} + +@Composable +private fun UserNotAuthenticatedDialog( + onShowCardWall: () -> Unit, + onCancel: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.user_not_authenticated_dialog_header), + bodyText = stringResource(R.string.user_not_authenticated_dialog_info), + confirmText = stringResource(R.string.user_not_authenticated_dialog_connect), + dismissText = stringResource(R.string.user_not_authenticated_dialog_cancel), + onDismissRequest = onCancel, + onConfirmRequest = onShowCardWall + ) +} + +@LightDarkPreview +@Composable +fun UserNotAuthenticatedDialogPreview() { + PreviewAppTheme { + UserNotAuthenticatedDialog( + onShowCardWall = {}, + onCancel = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/AvatarDimensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/AvatarDimensions.kt new file mode 100644 index 00000000..1c98a15e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/AvatarDimensions.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.model + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import de.gematik.ti.erp.app.theme.SizeDefaults + +sealed class AvatarDimensions { + data class Small( + val dimension: AvatarDimension = AvatarDimension( + SizeDefaults.fivefold, + SizeDefaults.double, + SizeDefaults.doubleHalf, + DpOffset(SizeDefaults.one, SizeDefaults.threeQuarter), + SizeDefaults.quarter, + SizeDefaults.oneHalf + ) + ) : AvatarDimensions() + + data class Default( + val dimension: AvatarDimension = AvatarDimension( + SizeDefaults.twelvefold, + SizeDefaults.triple, + SizeDefaults.fivefold, + DpOffset(SizeDefaults.oneHalf, SizeDefaults.oneHalf), + SizeDefaults.half, + SizeDefaults.double + ) + ) : AvatarDimensions() +} + +data class AvatarDimension( + val avatarSize: Dp, + val chooseSize: Dp, + val statusSize: Dp, + val statusOffset: DpOffset, + val statusBorder: Dp, + val iconSize: Dp +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt deleted file mode 100644 index 7f5f4c2d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui.model - -import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData - -object PrescriptionScreenData { - @Immutable - data class State( - val prescriptions: List, - val redeemedPrescriptions: List - ) - - val EmptyState = State(emptyList(), emptyList()) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionsScreenClickActions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionsScreenClickActions.kt new file mode 100644 index 00000000..e1038cea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionsScreenClickActions.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.model + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +data class PrescriptionsScreenContentClickAction( + val onClickLogin: (ProfilesUseCaseData.Profile) -> Unit, + val onClickAvatar: (ProfilesUseCaseData.Profile) -> Unit, + val onClickArchive: () -> Unit, + val onClickPrescription: (String) -> Unit, + val onChooseAuthenticationMethod: (ProfileIdentifier) -> Unit, + val onClickRedeem: () -> Unit, + val onClickRefresh: () -> Unit +) + +data class MultiProfileTopAppBarClickAction( + val onClickAddProfile: () -> Unit, + val onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, + val onClickAddScannedPrescription: () -> Unit, + val onSwitchActiveProfile: (ProfilesUseCaseData.Profile) -> Unit, + val onElevateTopAppBar: (Boolean) -> Unit +) + +data class ConsentClickAction( + val onGetChargeConsent: (ProfileIdentifier) -> Unit +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt index 27881e4c..2ac7b5b1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui.model @@ -36,9 +36,9 @@ object ScanData { } sealed class Info { - object Focus : Info() - object ErrorNotValid : Info() - object ErrorDuplicated : Info() + data object Focus : Info() + data object ErrorNotValid : Info() + data object ErrorDuplicated : Info() @Immutable data class Scanned(val nr: Int) : Info() @@ -46,8 +46,7 @@ object ScanData { @Immutable data class ActionBar(val totalNrOfPrescriptions: Int, val totalNrOfCodes: Int) { - fun shouldShow() = - totalNrOfPrescriptions > 0 && totalNrOfCodes > 0 + fun shouldShow() = totalNrOfPrescriptions > 0 && totalNrOfCodes > 0 } @Immutable @@ -84,8 +83,7 @@ object ScanData { data class State( val snackBar: ActionBar ) { - fun hasCodesToSave() = - snackBar.shouldShow() + fun hasCodesToSave() = snackBar.shouldShow() } val defaultState = State( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt index 5da2e8e3..c1fd7edc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.ui.model @@ -21,46 +21,63 @@ package de.gematik.ti.erp.app.prescription.ui.model import kotlinx.datetime.Instant sealed interface SentOrCompletedPhrase { - object RedeemedJustNow : SentOrCompletedPhrase - object SentJustNow : SentOrCompletedPhrase + data object RedeemedJustNow : SentOrCompletedPhrase + data object SentJustNow : SentOrCompletedPhrase + data object ProvidedJustNow : SentOrCompletedPhrase data class RedeemedMinutesAgo(val minutes: Long) : SentOrCompletedPhrase data class SentMinutesAgo(val minutes: Long) : SentOrCompletedPhrase + data class ProvidedMinutesAgo(val minutes: Long) : SentOrCompletedPhrase data class RedeemedHoursAgo(val on: Instant) : SentOrCompletedPhrase data class SentHoursAgo(val on: Instant) : SentOrCompletedPhrase + data class ProvidedHoursAgo(val on: Instant) : SentOrCompletedPhrase data class RedeemedOn(val on: Instant) : SentOrCompletedPhrase data class SentOn(val on: Instant) : SentOrCompletedPhrase + data class ProvidedOn(val on: Instant) : SentOrCompletedPhrase } private const val JustNowMinutes = 5L private const val MinutesAgo = 60L -fun sentOrCompleted(lastModified: Instant, now: Instant, completed: Boolean = false): SentOrCompletedPhrase { +fun sentOrCompleted( + lastModified: Instant, + now: Instant, + completed: + Boolean = false, + provided: Boolean = false +): SentOrCompletedPhrase { val dayDifference = (now - lastModified).inWholeDays val minDifference = (now - lastModified).inWholeMinutes return when { minDifference < JustNowMinutes -> if (completed) { SentOrCompletedPhrase.RedeemedJustNow + } else if (provided) { + SentOrCompletedPhrase.ProvidedJustNow } else { SentOrCompletedPhrase.SentJustNow } - minDifference < MinutesAgo -> if (completed) { SentOrCompletedPhrase.RedeemedMinutesAgo(minDifference) + } else if (provided) { + SentOrCompletedPhrase.ProvidedMinutesAgo(minDifference) } else { SentOrCompletedPhrase.SentMinutesAgo(minDifference) } dayDifference <= 0L -> if (completed) { SentOrCompletedPhrase.RedeemedHoursAgo(lastModified) + } else if (provided) { + SentOrCompletedPhrase.ProvidedHoursAgo(lastModified) } else { SentOrCompletedPhrase.SentHoursAgo(lastModified) } else -> if (completed) { SentOrCompletedPhrase.RedeemedOn(lastModified) + } else if (provided) { + SentOrCompletedPhrase.ProvidedOn(lastModified) } else { SentOrCompletedPhrase.SentOn(lastModified) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/OnlineRedeemPreferencesScreenPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/OnlineRedeemPreferencesScreenPreviewParameterProvider.kt new file mode 100644 index 00000000..77b49b45 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/OnlineRedeemPreferencesScreenPreviewParameterProvider.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewData.PharmacyOrders +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewData.emptyPharmacyOrders +import kotlinx.datetime.Instant + +class OnlineRedeemPreferencesScreenPreviewParameterProvider : PreviewParameterProvider> { + + override val values: Sequence> + get() = sequenceOf( + PharmacyOrders, + emptyPharmacyOrders + ) +} + +object OnlineRedeemPreferencesScreenPreviewData { + val time = Instant.parse("2021-11-25T15:20:00Z") + + val PharmacyOrders = listOf( + PharmacyUseCaseData.PrescriptionOrder( + taskId = "1", + accessCode = "ABC123", + title = "Prescription 1", + isSelfPayerPrescription = false, + index = 1, + timestamp = time, + substitutionsAllowed = true, + isScanned = false + ), + PharmacyUseCaseData.PrescriptionOrder( + taskId = "2", + accessCode = "XYZ456", + title = "Prescription 2", + isSelfPayerPrescription = true, + index = 2, + timestamp = time, + substitutionsAllowed = false, + isScanned = true + ), + PharmacyUseCaseData.PrescriptionOrder( + taskId = "2", + accessCode = "XYZ456", + title = "Prescription 2", + isSelfPayerPrescription = true, + index = 2, + timestamp = time, + substitutionsAllowed = false, + isScanned = true + ) + ) + val emptyPharmacyOrders = emptyList() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionScreenPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionScreenPreviewParameterProvider.kt new file mode 100644 index 00000000..b7c8c8b1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionScreenPreviewParameterProvider.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.LayoutDirection +import de.gematik.ti.erp.app.app.ApplicationInnerPadding +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.mainscreen.model.MultiProfileAppBarFlowWrapper +import de.gematik.ti.erp.app.prescription.ui.model.ConsentClickAction +import de.gematik.ti.erp.app.prescription.ui.model.MultiProfileTopAppBarClickAction +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionsScreenContentClickAction +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow + +data class PrescriptionScreenPreviewData( + val name: String, + val multiProfileAppBarFlowWrapper: MultiProfileAppBarFlowWrapper, + val profileData: UiState, + val activePrescription: UiState>, + val isArchivedEmpty: Boolean, + val hasRedeemableTasks: Boolean, + val consentState: ConsentState = ConsentState.ValidState.NotGranted, + val isTopBarElevated: Boolean = false, + val fabPadding: ApplicationInnerPadding = ApplicationInnerPadding(layoutDirection = LayoutDirection.Ltr), + val prescriptionsClickAction: PrescriptionsScreenContentClickAction = PrescriptionsScreenContentClickAction( + onClickLogin = {}, + onClickAvatar = {}, + onClickArchive = {}, + onClickPrescription = {}, + onChooseAuthenticationMethod = {}, + onClickRedeem = {}, + onClickRefresh = {} + ), + val consentClickAction: ConsentClickAction = ConsentClickAction( + onGetChargeConsent = {} + ), + val topAppBarClickAction: MultiProfileTopAppBarClickAction = MultiProfileTopAppBarClickAction( + onClickAddProfile = {}, + onClickChangeProfileName = {}, + onClickAddScannedPrescription = {}, + onSwitchActiveProfile = {}, + onElevateTopAppBar = {} + ) +) + +class PrescriptionScreenPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PrescriptionScreenPreviewData( + name = "empty-prescriptions-user-logged-out", + multiProfileAppBarFlowWrapper = MultiProfileAppBarFlowWrapper( + activeProfile = MutableStateFlow(MOCK_MODEL_PROFILE), + existingProfiles = MutableStateFlow(listOf(MOCK_MODEL_PROFILE, MOCK_MODEL_PROFILE_2)), + isProfileRefreshing = MutableStateFlow(false) + ), + profileData = UiState.Data(MOCK_MODEL_PROFILE), + activePrescription = UiState.Empty(), + isArchivedEmpty = true, + hasRedeemableTasks = false + ), + PrescriptionScreenPreviewData( + name = "with-prescriptions-user-logged-in", + multiProfileAppBarFlowWrapper = MultiProfileAppBarFlowWrapper( + activeProfile = MutableStateFlow(MOCK_MODEL_PROFILE_LOGGED_IN), + existingProfiles = MutableStateFlow(listOf(MOCK_MODEL_PROFILE_LOGGED_IN)), + isProfileRefreshing = MutableStateFlow(false) + ), + profileData = UiState.Data(MOCK_MODEL_PROFILE_LOGGED_IN), + activePrescription = UiState.Data( + listOf( + MOCK_PRESCRIPTION_SELF_PAYER, + MOCK_PRESCRIPTION_DIRECT_ASSIGNMENT, + MOCK_PRESCRIPTION_EXPIRED, + MOCK_PRESCRIPTION_DELETED + ) + ), + isArchivedEmpty = false, + hasRedeemableTasks = true, + isTopBarElevated = true + ), + /* Todo: Date conflicts causing below test data to fail. After refactoring the component, this test data should be enabled. + PrescriptionScreenPreviewData( + name = "with-ready-prescriptions-user-logged-in", + multiProfileAppBarFlowWrapper = MultiProfileAppBarFlowWrapper( + activeProfile = MutableStateFlow(MOCK_MODEL_PROFILE_LOGGED_IN), + existingProfiles = MutableStateFlow(listOf(MOCK_MODEL_PROFILE_LOGGED_IN)), + isProfileRefreshing = MutableStateFlow(false) + ), + profileData = UiState.Data(MOCK_MODEL_PROFILE_LOGGED_IN), + activePrescription = UiState.Data( + listOf(MOCK_PRESCRIPTION_READY) + ), + isArchivedEmpty = false, + hasRedeemableTasks = true + ), + ), + */ + PrescriptionScreenPreviewData( + name = "with-prescriptions-user-invalid", + multiProfileAppBarFlowWrapper = MultiProfileAppBarFlowWrapper( + activeProfile = MutableStateFlow(MOCK_MODEL_PROFILE_LOGGED_INVALID), + existingProfiles = MutableStateFlow(listOf(MOCK_MODEL_PROFILE_LOGGED_INVALID, MOCK_MODEL_PROFILE)), + isProfileRefreshing = MutableStateFlow(false) + ), + profileData = UiState.Data(MOCK_MODEL_PROFILE_LOGGED_INVALID), + activePrescription = UiState.Data( + listOf( + MOCK_PRESCRIPTION_PENDING, + MOCK_PRESCRIPTION_IN_PROGRESS, + MOCK_PRESCRIPTION_LATER_REDEEMABLE, + MOCK_PRESCRIPTION_OTHER + ) + ), + isArchivedEmpty = true, + hasRedeemableTasks = false + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionStatePreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionStatePreviewParameter.kt new file mode 100644 index 00000000..fce6c2ff --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionStatePreviewParameter.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +data class PrescriptionStatePreview( + val name: String, // a description for better understanding + val prescriptionState: SyncedTaskData.SyncedTask.TaskState, + val now: Instant +) + +// see https://wiki.gematik.de/pages/viewpage.action?pageId=488575023&preview=/488575023/532687126/G%C3%BCltigkeitsangaben.xlsx +private val creationDateTime = Instant.parse("2023-01-01T16:20:00Z") + +private val creationDate = creationDateTime.toLocalDateTime( + TimeZone.of("Europe/Berlin") +).date.atStartOfDayIn( + TimeZone.of("Europe/Berlin") +) +private val acceptUntil = creationDateTime.plus(27.days).toLocalDateTime( + TimeZone.of("Europe/Berlin") +).date.atStartOfDayIn( + TimeZone.of("Europe/Berlin") +) +private val expiryDate = Instant.parse("2023-04-01T16:20:00Z").toLocalDateTime( + TimeZone.of("Europe/Berlin") +).date.atStartOfDayIn( + TimeZone.of("Europe/Berlin") +) + +val prescriptionStatePreviews: Sequence + get() = sequenceOf( + PrescriptionStatePreview( + name = "ready state with 0 days gone (valid for 27 days more than today)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate + ), + PrescriptionStatePreview( + name = "ready state with 24 days gone (valid for 3 days more than today)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(24.days) + ), + PrescriptionStatePreview( + name = "ready state with 25 days gone (valid for 2 days more than today)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(25.days) + ), + PrescriptionStatePreview( + name = "ready state with 26 days gone (valid for 1 day more than today)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(26.days) + ), + PrescriptionStatePreview( + name = "ready state with 27 days gone (only valid today)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(27.days) + ), + PrescriptionStatePreview( + name = "ready state with 28 days gone (61 days more than today as self payer)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(28.days) + ), + PrescriptionStatePreview( + name = "ready state with 85 days gone (valid for 3 days as self payer)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(85.days) + ), + PrescriptionStatePreview( + name = "ready state with 86 days gone (valid for 2 days as self payer)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(86.days) + ), + PrescriptionStatePreview( + name = "ready state with 88 days gone (valid until tomorrow as self payer)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(87.days) + ), + PrescriptionStatePreview( + name = "ready state with 88 days gone (only valid today as self payer)", + prescriptionState = SyncedTaskData.SyncedTask.Ready( + expiresOn = expiryDate, + acceptUntil = acceptUntil + ), + now = creationDate.plus(88.days) + ), + PrescriptionStatePreview( + name = "provided state after 5 minutes", + prescriptionState = SyncedTaskData.SyncedTask.Provided( + lastMedicationDispense = creationDate.plus(5.minutes) + ), + now = creationDate.plus(10.minutes) + ), + PrescriptionStatePreview( + name = "provided state after 10 days", + prescriptionState = SyncedTaskData.SyncedTask.Provided( + lastMedicationDispense = creationDate.plus(10.days) + ), + now = creationDate.plus(20.days) + ), + PrescriptionStatePreview( + name = "provided state after 2 hours", + prescriptionState = SyncedTaskData.SyncedTask.Provided( + lastMedicationDispense = creationDate.plus(1.hours) + ), + now = creationDate.plus(3.hours) + ) + + // toDo: add more states such as later redeemable for multiple prescriptions and so on + ) + +class PrescriptionStatePreviewParameterProvider : PreviewParameterProvider { + override val values = prescriptionStatePreviews +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionsArchiveScreenPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionsArchiveScreenPreviewParameterProvider.kt new file mode 100644 index 00000000..4f7b5764 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PrescriptionsArchiveScreenPreviewParameterProvider.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.days + +class PrescriptionsArchiveScreenPreviewParameterProvider : PreviewParameterProvider>> { + override val values: Sequence>> + get() = sequenceOf( + UiState.Empty(), + UiState.Error(Throwable("Error")), + UiState.Data( + PrescriptionsArchiveScreenPreviewData.mockPrescriptionsUiState + ) + ) +} + +object PrescriptionsArchiveScreenPreviewData { + val now: Instant = Instant.parse("2023-11-20T15:20:00Z") + + private val mockPrescriptions = listOf( + Prescription.SyncedPrescription( + taskId = "1", + name = "Painkillers Medication 1", + redeemedOn = now - 2.days, + expiresOn = now - 3.days, + state = SyncedTaskData.SyncedTask.Expired( + expiredOn = now - 1.days + ), + isIncomplete = true, + organization = "Health Organization A", + authoredOn = now - 10.days, + acceptUntil = now + 20.days, + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isSelfPayPrescription = true, + isPartOfMultiplePrescription = false, + numerator = "1", + denominator = "5", + start = now - 5.days + ) + ), + Prescription.SyncedPrescription( + taskId = "2", + name = "Painkillers Medication 2", + redeemedOn = now - 10.days, + expiresOn = null, + state = SyncedTaskData.SyncedTask.Deleted( + lastModified = now - 5.days + ), + isIncomplete = true, + organization = "Health Organization B", + authoredOn = now - 15.days, + acceptUntil = null, + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isSelfPayPrescription = true, + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = null + ) + ), + Prescription.SyncedPrescription( + taskId = "3", + name = "Painkillers Medication 3", + redeemedOn = null, + expiresOn = now - 1.days, + state = SyncedTaskData.SyncedTask.Expired( + expiredOn = now - 1.days + ), + isIncomplete = false, + organization = "Health Organization C", + authoredOn = now - 40.days, + acceptUntil = now - 40.days, + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isSelfPayPrescription = false, + isPartOfMultiplePrescription = true, + numerator = "2", + denominator = "3", + start = now - 35.days + ) + ), + Prescription.ScannedPrescription( + taskId = "4", + name = "Painkillers Medication 4", + redeemedOn = now - 5.days, + scannedOn = now - 10.days, + index = 1, + communications = emptyList() + ) + ) + + val mockPrescriptionsUiState: List = + mockPrescriptions +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PreviewData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PreviewData.kt new file mode 100644 index 00000000..bfe629af --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/preview/PreviewData.kt @@ -0,0 +1,281 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.preview + +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.datetime.Instant + +val MOCK_MODEL_PROFILE = ProfilesUseCaseData.Profile( + id = "id-1", + name = "first profile", + insurance = ProfileInsuranceInformation( + insurantName = "insurantName", + insuranceIdentifier = "insuranceIdentifier", + insuranceName = "insuranceName", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = byteArrayOf(0x00, 0x01, 0x02), + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = null +) + +val MOCK_MODEL_PROFILE_LOGGED_IN = ProfilesUseCaseData.Profile( + id = "id-1", + name = "logged-in", + insurance = ProfileInsuranceInformation( + insurantName = "insurantName", + insuranceIdentifier = "insuranceIdentifier", + insuranceName = "insuranceName", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SUN_DEW, + avatar = ProfilesData.Avatar.FemaleDoctor, + image = byteArrayOf(0x00, 0x01, 0x02), + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = IdpData.ExternalAuthenticationToken( + token = IdpData.SingleSignOnToken( + token = "token", + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + validOn = Instant.parse("2023-08-01T10:00:00Z") + ), + authenticatorName = "authenticatorName", + authenticatorId = "authenticatorId" + ) +) + +val MOCK_MODEL_PROFILE_LOGGED_INVALID = ProfilesUseCaseData.Profile( + id = "id-invalid", + name = "token-null", + insurance = ProfileInsuranceInformation( + insurantName = "insurantName", + insuranceIdentifier = "insuranceIdentifier", + insuranceName = "insuranceName", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SUN_DEW, + avatar = ProfilesData.Avatar.WomanWithPhone, + image = byteArrayOf(0x00, 0x01, 0x02), + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = IdpData.ExternalAuthenticationToken( + token = null, + authenticatorName = "authenticatorName", + authenticatorId = "authenticatorId" + ) +) + +val MOCK_MODEL_PROFILE_2 = ProfilesUseCaseData.Profile( + id = "id-2", + name = "second profile", + insurance = ProfileInsuranceInformation( + insurantName = "insurantName", + insuranceIdentifier = "insuranceIdentifier", + insuranceName = "insuranceName", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.FemaleDoctor, + image = byteArrayOf(0x00, 0x01, 0x02), + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = null +) + +val MOCK_PRESCRIPTION_SELF_PAYER = Prescription.SyncedPrescription( + taskId = "Amlodipine", + name = "Amlodipine", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Ready( + expiresOn = Instant.parse("2023-08-01T10:00:00Z"), + acceptUntil = Instant.parse("2024-08-01T10:00:00Z") + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("2024-08-01T10:00:00Z"), // decides self-payment + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_DIRECT_ASSIGNMENT = MOCK_PRESCRIPTION_SELF_PAYER.copy( + taskId = "Atorvastatin", + name = "Atorvastatin", + isDirectAssignment = true +) + +val MOCK_PRESCRIPTION_EXPIRED = Prescription.SyncedPrescription( + taskId = "Cetirizine", + name = "Cetirizine", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Expired(expiredOn = Instant.parse("2024-08-01T10:00:00Z")), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_DELETED = Prescription.SyncedPrescription( + taskId = "Glipizide", + name = "Glipizide", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Deleted(lastModified = Instant.parse("2024-08-01T10:00:00Z")), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_PENDING = Prescription.SyncedPrescription( + taskId = "Metformin", + name = "Metformin", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Pending( + sentOn = Instant.parse("2024-08-01T10:00:00Z"), + toTelematikId = "toTelematikId" + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_IN_PROGRESS = Prescription.SyncedPrescription( + taskId = "Montelukast", + name = "Montelukast", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.InProgress( + lastModified = Instant.parse("2024-08-01T10:00:00Z") + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_LATER_REDEEMABLE = Prescription.SyncedPrescription( + taskId = "Zolpidem", + name = "Zolpidem", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.LaterRedeemable( + redeemableOn = Instant.parse("2024-08-01T10:00:00Z") + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_OTHER = Prescription.SyncedPrescription( + taskId = "Fluoxetine", + name = "Fluoxetine", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Other( + state = SyncedTaskData.TaskStatus.Failed, + lastModified = Instant.parse("2024-08-01T10:00:00Z") + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) + +val MOCK_PRESCRIPTION_READY = Prescription.SyncedPrescription( + taskId = "Fluoxetine", + name = "Fluoxetine", + redeemedOn = null, + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + state = SyncedTaskData.SyncedTask.Ready( + expiresOn = Instant.parse("3024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z") + ), + isIncomplete = false, + organization = "MOCK_PRACTITIONER_NAME", + authoredOn = Instant.parse("2024-08-01T10:00:00Z"), + acceptUntil = Instant.parse("3024-08-01T10:00:00Z"), + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation( + isPartOfMultiplePrescription = false, + numerator = null, + denominator = null, + start = Instant.parse("2024-08-01T10:00:00Z") + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/ArchiveScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/ArchiveScreen.kt new file mode 100644 index 00000000..77c6dd9a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/ArchiveScreen.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.screen + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.prescription.presentation.rememberPrescriptionsController +import de.gematik.ti.erp.app.prescription.ui.components.CardPaddingModifier +import de.gematik.ti.erp.app.prescription.ui.components.FullDetailMedication +import de.gematik.ti.erp.app.prescription.ui.components.LowDetailMedication +import de.gematik.ti.erp.app.prescription.ui.preview.PrescriptionsArchiveScreenPreviewParameterProvider +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.EmptyScreenComponent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter + +class PrescriptionsArchiveScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val controller = rememberPrescriptionsController() + val listState = rememberLazyListState() + val archivedPrescriptions by controller.archivedPrescriptions.collectAsStateWithLifecycle() + + BackHandler { + navController.popBackStack() + } + + PrescriptionsArchiveScreenScaffold( + listState = listState, + archivedPrescriptions = archivedPrescriptions, + onBack = { navController.popBackStack() }, + onOpenPrescriptionDetailScreen = { + navController.navigate( + PrescriptionDetailRoutes.PrescriptionDetailScreen.path( + taskId = it + ) + ) + } + ) + } +} + +@Composable +private fun PrescriptionsArchiveScreenScaffold( + listState: LazyListState, + archivedPrescriptions: UiState>, + onBack: () -> Unit, + onOpenPrescriptionDetailScreen: (String) -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.archive_screen_title), + listState = listState, + onBack = onBack, + navigationMode = NavigationBarMode.Back + ) { + UiStateMachine( + state = archivedPrescriptions, + onError = { + ErrorScreenComponent() + }, + onEmpty = { + PrescriptionsArchiveEmptyScreenContent() + }, + onLoading = { + Center { + CircularProgressIndicator() + } + } + ) { prescriptions -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag(TestTag.Prescriptions.Archive.Content), + state = listState, + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { SpacerXXLarge() } + + prescriptions.forEachIndexed { index, prescription -> + item(key = "prescription-${prescription.taskId}") { + val previousPrescriptionRedeemedOn = + prescriptions.getOrNull(index - 1) + ?.redeemedOrExpiredOn() + ?.toLocalDateTime(TimeZone.currentSystemDefault()) + + val redeemedOn = prescription.redeemedOrExpiredOn() + .toLocalDateTime(TimeZone.currentSystemDefault()) + + val yearChanged = remember { + previousPrescriptionRedeemedOn?.year != redeemedOn.year + } + + if (yearChanged) { + val instantOfArchivedPrescription = remember { + val dateFormatter = DateTimeFormatter.ofPattern("yyyy") + redeemedOn.toJavaLocalDateTime().format(dateFormatter) + } + + Text( + text = instantOfArchivedPrescription, + style = AppTheme.typography.h6, + modifier = Modifier + .fillMaxWidth() + .then(CardPaddingModifier) + ) + } + + when (prescription) { + is ScannedPrescription -> + LowDetailMedication( + modifier = CardPaddingModifier, + prescription, + onClick = { + onOpenPrescriptionDetailScreen(prescription.taskId) + } + ) + + is SyncedPrescription -> + FullDetailMedication( + prescription, + modifier = CardPaddingModifier, + onClick = { + onOpenPrescriptionDetailScreen(prescription.taskId) + } + ) + } + } + } + } + } + } +} + +@Composable +fun PrescriptionsArchiveEmptyScreenContent( + modifier: Modifier = Modifier +) = + EmptyScreenComponent( + modifier = modifier, + title = stringResource(R.string.prescription_archive_empty_screen_title), + body = stringResource(R.string.prescription_archive_empty_screen_body), + button = {} + ) + +@LightDarkPreview +@Composable +fun PrescriptionsArchiveScreenPreview( + @PreviewParameter(PrescriptionsArchiveScreenPreviewParameterProvider::class) + archivedPrescriptions: UiState> +) { + val listState = rememberLazyListState() + + PreviewAppTheme { + PrescriptionsArchiveScreenScaffold( + listState = listState, + archivedPrescriptions = archivedPrescriptions, + onBack = { }, + onOpenPrescriptionDetailScreen = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreen.kt new file mode 100644 index 00000000..689de367 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreen.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UsingMaterialAndMaterial3Libraries") + +package de.gematik.ti.erp.app.prescription.ui.screen + +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.base.presentation.rememberGetActiveProfileController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.presentation.rememberConsentController +import de.gematik.ti.erp.app.prescription.ui.components.CommonDrawerScreenContent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.FullScreenLoadingIndicator +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar + +/** + * This drawer is hidden from the user if the user clicks on one of the buttons when it is shown + */ +class GrantConsentBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = true) { + @Composable + override fun Content() { + val controller = rememberGetActiveProfileController() + val consentController = rememberConsentController() + val profileData by controller.activeProfile.collectAsStateWithLifecycle() + + val snackbar = LocalSnackbar.current + val consentGrantedInfo = stringResource(R.string.consent_granted_info) + + UiStateMachine( + state = profileData, + onError = { + ErrorScreenComponent() + }, + onLoading = { + FullScreenLoadingIndicator() + }, + onContent = { profile -> + GrantConsentScreenContent( + onClickGrantConsent = { + consentController.saveConsentDrawerShown(profile.id) + consentController.grantChargeConsent(profile.id) + snackbar.show( + text = consentGrantedInfo, + actionTextId = R.string.consent_action_to_invoices, + onClickAction = { + profileData.data?.id?.let { + navController.navigate(PkvRoutes.InvoiceListScreen.path(it)) + } + } + ) + navController.popBackStack() + }, + onCancel = { + consentController.saveConsentDrawerShown(profile.id) + navController.popBackStack() + } + ) + } + ) + } +} + +@Composable +private fun GrantConsentScreenContent( + onClickGrantConsent: () -> Unit, + onCancel: () -> Unit +) { + CommonDrawerScreenContent( + modifierPrimary = Modifier + .testTag(TestTag.Main.MainScreenBottomSheet.GetConsentButton) + .wrapContentSize(), + header = stringResource(R.string.give_consent_bottom_sheet_header), + info = stringResource(R.string.give_consent_bottom_sheet_info), + image = painterResource(R.drawable.pharmacist_circle_blue), + connectButtonText = stringResource(R.string.give_consent_bottom_sheet_activate), + cancelButtonText = stringResource(R.string.give_consent_bottom_sheet_activate_later), + onClickConnect = onClickGrantConsent, + onCancel = onCancel + ) +} + +@LightDarkPreview +@Composable +fun GrantConsentBottomSheetScreenPreview() { + PreviewAppTheme { + GrantConsentScreenContent( + onClickGrantConsent = {}, + onCancel = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreen.kt new file mode 100644 index 00000000..d90baa3d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreen.kt @@ -0,0 +1,674 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UsingMaterialAndMaterial3Libraries") + +package de.gematik.ti.erp.app.prescription.ui.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.FabPosition +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarResult +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.app.ApplicationInnerPadding +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.base.model.DownloadResourcesState.Companion.isFinished +import de.gematik.ti.erp.app.base.model.DownloadResourcesState.NotStarted +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.model.MultiProfileAppBarFlowWrapper +import de.gematik.ti.erp.app.mainscreen.ui.MultiProfileTopAppBar +import de.gematik.ti.erp.app.mainscreen.ui.RedeemFloatingActionButton +import de.gematik.ti.erp.app.mlkit.navigation.MlKitRoutes +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.pkv.presentation.ConsentValidator +import de.gematik.ti.erp.app.pkv.presentation.rememberConsentController +import de.gematik.ti.erp.app.pkv.ui.screens.HandleConsentState +import de.gematik.ti.erp.app.prescription.detail.navigation.PrescriptionDetailRoutes +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.prescription.presentation.rememberPrescriptionsController +import de.gematik.ti.erp.app.prescription.ui.components.UserNotAuthenticatedDialog +import de.gematik.ti.erp.app.prescription.ui.components.archiveSection +import de.gematik.ti.erp.app.prescription.ui.components.emptyContentSection +import de.gematik.ti.erp.app.prescription.ui.components.prescriptionContentSection +import de.gematik.ti.erp.app.prescription.ui.components.profileConnectorSection +import de.gematik.ti.erp.app.prescription.ui.model.ConsentClickAction +import de.gematik.ti.erp.app.prescription.ui.model.MultiProfileTopAppBarClickAction +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionsScreenContentClickAction +import de.gematik.ti.erp.app.prescription.ui.preview.PrescriptionScreenPreviewData +import de.gematik.ti.erp.app.prescription.ui.preview.PrescriptionScreenPreviewParameterProvider +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.pulltorefresh.PullToRefresh +import de.gematik.ti.erp.app.pulltorefresh.extensions.trigger +import de.gematik.ti.erp.app.pulltorefresh.extensions.triggerEnd +import de.gematik.ti.erp.app.pulltorefresh.extensions.triggerStart +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbarScaffold +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +const val ZERO_DAYS_LEFT = 0 +const val ONE_DAY_LEFT = 1 +const val TWO_DAYS_LEFT = 2 + +class PrescriptionsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @OptIn(ExperimentalMaterial3Api::class) + @Suppress("CyclomaticComplexMethod") + @Composable + override fun Content() { + val fabPadding = (LocalActivity.current as? BaseActivity)?.applicationInnerPadding + val controller = rememberPrescriptionsController() + val consentController = rememberConsentController() + + val pullToRefreshState = rememberPullToRefreshState() + val snackbar = LocalSnackbarScaffold.current + val dialog = LocalDialog.current + val intentHandler = LocalIntentHandler.current + val scope = rememberCoroutineScope() + + val actionString = stringResource(R.string.consent_action_to_invoices) + val consentRevokedInfo = stringResource(R.string.consent_revoked_info) + val consentGrantedInfo = stringResource(R.string.consent_granted_info) + + val activePrescriptions by controller.activePrescriptions.collectAsStateWithLifecycle() + val isArchiveEmpty by controller.isArchiveEmpty.collectAsStateWithLifecycle() + val hasRedeemableTasks by controller.hasRedeemableTasks.collectAsStateWithLifecycle() + + val profileData by controller.activeProfile.collectAsStateWithLifecycle() + val resourcesDownloadedState by controller.resourcesDownloadedState.collectAsState(NotStarted) + + val mlKitAccepted by controller.isMLKitAccepted.collectAsStateWithLifecycle() + val consentState by consentController.consentState.collectAsStateWithLifecycle() + var topBarElevated by remember { mutableStateOf(true) } + + DisposableEffect(Unit) { + onDispose { controller.trackPrescriptionCounts() } + } + + with(controller) { + showCardWallEvent.listen { profileId -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(profileId)) + } + showCardWallWithFilledCanEvent.listen { cardWallData -> + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = cardWallData.profileId, + can = cardWallData.can + ) + ) + } + refreshEvent.listen { state -> + when { + state -> pullToRefreshState.triggerStart() + else -> pullToRefreshState.triggerEnd() + } + } + showGidEvent.listen { gidData -> + navController.navigate( + CardWallRoutes.CardWallIntroScreen.pathWithGid( + profileIdentifier = gidData.profileId, + gidEventData = gidData + ) + ) + } + } + + navBackStackEntry.onReturnAction(PrescriptionRoutes.PrescriptionsScreen) { + controller.refreshDownload() + } + + LaunchedEffect(true) { + if (controller.shouldShowWelcomeDrawer.first()) { + navController.navigate(PrescriptionRoutes.WelcomeDrawerBottomSheetScreen.path()) + } + if (controller.shouldShowGrantConsentDrawer.first()) { + navController.navigate(PrescriptionRoutes.GrantConsentBottomSheetScreen.path()) + } + } + + LaunchedEffect(resourcesDownloadedState) { + if (resourcesDownloadedState.isFinished()) controller.disablePrescriptionRefresh() + } + + LaunchedEffect(Unit) { + intentHandler.gidSuccessfulIntent.collectLatest { + controller.refreshDownload() + } + } + + UserNotAuthenticatedDialog( + event = controller.onUserNotAuthenticatedErrorEvent, + dialogScaffold = dialog, + onShowCardWall = { + profileData.data?.let { activeProfile -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(activeProfile.id)) + } + } + ) + + // TODO: handle Consent not Granted on PrescriptionScreen, InvoiceListScreen and PrescriptionDetailsScreen + HandleConsentState( + consentState = consentState, + dialog = dialog, + onShowCardWall = { + profileData.data?.let { activeProfile -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(activeProfile.id)) + } + }, + onRetry = { consentContext -> + profileData.data?.let { activeProfile -> + when (consentContext) { + ConsentContext.GetConsent -> consentController.getChargeConsent(activeProfile.id) + ConsentContext.GrantConsent -> consentController.grantChargeConsent(activeProfile.id) + ConsentContext.RevokeConsent -> {} // revoke is not available on mainScreen + } + } + }, + onConsentGranted = { + scope.launch { + val result = + snackbar.showSnackbar( + message = consentGrantedInfo, + actionLabel = actionString + ) + when (result) { + SnackbarResult.Dismissed -> {} + SnackbarResult.ActionPerformed -> + profileData.data?.id?.let { + navController.navigate(PkvRoutes.InvoiceListScreen.path(it)) + } + } + } + }, + onConsentRevoked = { + scope.launch { + val result = + snackbar.showSnackbar( + message = consentRevokedInfo, + actionLabel = actionString + ) + when (result) { + SnackbarResult.Dismissed -> {} + SnackbarResult.ActionPerformed -> + profileData.data?.id?.let { + navController.navigate(PkvRoutes.InvoiceListScreen.path(it)) + } + } + } + } + ) + + PrescriptionsScreenScaffold( + pullToRefreshState = pullToRefreshState, + listState = listState, + isTopBarElevated = topBarElevated, + fabPadding = fabPadding, + multiProfileData = controller.multiProfileData, + profileData = profileData, + activePrescriptions = activePrescriptions, + isArchiveEmpty = isArchiveEmpty, + hasRedeemableTasks = hasRedeemableTasks, + consentState = consentState, + topAppBarClickAction = MultiProfileTopAppBarClickAction( + onClickAddProfile = { navController.navigate(ProfileRoutes.ProfileAddNameBottomSheetScreen.path()) }, + onClickChangeProfileName = { profile -> navController.navigate(ProfileRoutes.ProfileEditNameBottomSheetScreen.path(profile.id)) }, + onClickAddScannedPrescription = { + when { + mlKitAccepted -> navController.navigate(PrescriptionRoutes.PrescriptionScanScreen.path()) + else -> navController.navigate(MlKitRoutes.MlKitScreen.path()) + } + }, + onSwitchActiveProfile = { profile -> + controller.disablePrescriptionRefresh() + controller.switchActiveProfile(profile.id) + }, + onElevateTopAppBar = { topBarElevated = it } + ), + prescriptionClickAction = PrescriptionsScreenContentClickAction( + onClickLogin = { profile -> controller.chooseAuthenticationMethod(profile.id) }, + onClickAvatar = { profile -> navController.navigate(ProfileRoutes.ProfileEditPictureBottomSheetScreen.path(profile.id)) }, + onClickArchive = { navController.navigate(PrescriptionRoutes.PrescriptionsArchiveScreen.path()) }, + onClickPrescription = { taskId -> navController.navigate(PrescriptionDetailRoutes.PrescriptionDetailScreen.path(taskId)) }, + onChooseAuthenticationMethod = { profileId -> controller.chooseAuthenticationMethod(profileId) }, + onClickRedeem = { navController.navigate(RedeemRoutes.RedeemMethodSelection.path()) }, + onClickRefresh = controller::refreshDownload + ), + consentClickAction = ConsentClickAction( + onGetChargeConsent = { profileId -> consentController.getChargeConsent(profileId) } + ) + ) + + Box( + Modifier + .fillMaxSize() + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + Scaffold( + isFloatingActionButtonDocked = true, + floatingActionButtonPosition = FabPosition.End, + topBar = { + MultiProfileTopAppBar( + multiProfileData = controller.multiProfileData, + elevated = topBarElevated, + onClickAddProfile = { navController.navigate(ProfileRoutes.ProfileAddNameBottomSheetScreen.path()) }, + onClickChangeProfileName = { profile -> navController.navigate(ProfileRoutes.ProfileEditNameBottomSheetScreen.path(profile.id)) }, + onClickAddPrescription = { + when { + mlKitAccepted -> navController.navigate(PrescriptionRoutes.PrescriptionScanScreen.path()) + else -> navController.navigate(MlKitRoutes.MlKitScreen.path()) + } + }, + switchActiveProfile = { profile -> + controller.disablePrescriptionRefresh() + controller.switchActiveProfile(profile.id) + } + ) + }, + floatingActionButton = { + if (hasRedeemableTasks) { + RedeemFloatingActionButton( + modifier = fabPadding?.applicationScaffoldPadding?.let { Modifier.padding(it) } ?: Modifier, + onClick = { + navController.navigate(RedeemRoutes.RedeemMethodSelection.path()) + } + ) + } + } + ) { + Column { + ProfileSection( + profileData = profileData, + consentState = consentState, + pullToRefreshState = pullToRefreshState, + onRefresh = { + controller.refreshDownload() + }, + onGetConsent = { profileId -> + consentController.getChargeConsent(profileId) + }, + onChooseAuthenticationMethod = { profileId -> + controller.chooseAuthenticationMethod(profileId) + } + ) + PrescriptionsSection( + modifier = Modifier.padding(it), + listState = listState, + activeProfile = profileData, + activePrescriptions = activePrescriptions, + isArchiveEmpty = isArchiveEmpty, + onClickRefresh = controller::refreshDownload, + onClickLogin = { + profileData.data?.let { activeProfile -> + controller.chooseAuthenticationMethod(activeProfile.id) + } + }, + onClickAvatar = { + profileData.data?.let { activeProfile -> + navController.navigate( + ProfileRoutes.ProfileEditPictureBottomSheetScreen.path(activeProfile.id) + ) + } + }, + onElevateTopBar = { topBarElevated = it }, + onClickArchive = { + navController.navigate(PrescriptionRoutes.PrescriptionsArchiveScreen.path()) + }, + onClickPrescription = { taskId -> + navController.navigate( + PrescriptionDetailRoutes.PrescriptionDetailScreen.path(taskId) + ) + } + ) + } + } + PullToRefresh( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = SizeDefaults.sixfoldAndQuarter), + pullToRefreshState = pullToRefreshState + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrescriptionsScreenScaffold( + pullToRefreshState: PullToRefreshState, + listState: LazyListState, + multiProfileData: MultiProfileAppBarFlowWrapper, + profileData: UiState, + activePrescriptions: UiState>, + fabPadding: ApplicationInnerPadding?, + isTopBarElevated: Boolean, + hasRedeemableTasks: Boolean, + consentState: ConsentState, + isArchiveEmpty: Boolean, + prescriptionClickAction: PrescriptionsScreenContentClickAction, + topAppBarClickAction: MultiProfileTopAppBarClickAction, + consentClickAction: ConsentClickAction +) { + Box( + Modifier + .fillMaxSize() + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + Scaffold( + isFloatingActionButtonDocked = true, + floatingActionButtonPosition = FabPosition.End, + topBar = { + MultiProfileTopAppBar( + multiProfileData = multiProfileData, + elevated = isTopBarElevated, + onClickAddProfile = topAppBarClickAction.onClickAddProfile, + onClickChangeProfileName = topAppBarClickAction.onClickChangeProfileName, + onClickAddPrescription = topAppBarClickAction.onClickAddScannedPrescription, + switchActiveProfile = topAppBarClickAction.onSwitchActiveProfile + ) + }, + floatingActionButton = { + if (hasRedeemableTasks) { + RedeemFloatingActionButton( + modifier = fabPadding?.applicationScaffoldPadding?.let { Modifier.padding(it) } ?: Modifier, + onClick = prescriptionClickAction.onClickRedeem + ) + } + } + ) { + Column { + ProfileSection( + profileData = profileData, + consentState = consentState, + pullToRefreshState = pullToRefreshState, + onRefresh = prescriptionClickAction.onClickRefresh, + onGetConsent = consentClickAction.onGetChargeConsent, + onChooseAuthenticationMethod = prescriptionClickAction.onChooseAuthenticationMethod + ) + PrescriptionsSection( + modifier = Modifier.padding(it), + listState = listState, + activeProfile = profileData, + activePrescriptions = activePrescriptions, + isArchiveEmpty = isArchiveEmpty, + onClickRefresh = prescriptionClickAction.onClickRefresh, + onClickLogin = { + profileData.data?.let { activeProfile -> + prescriptionClickAction.onClickLogin(activeProfile) + } + }, + onClickAvatar = { + profileData.data?.let { activeProfile -> + prescriptionClickAction.onClickAvatar(activeProfile) + } + }, + onClickArchive = prescriptionClickAction.onClickArchive, + onClickPrescription = prescriptionClickAction.onClickPrescription, + onElevateTopBar = topAppBarClickAction.onElevateTopAppBar + ) + } + } + PullToRefresh( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = SizeDefaults.sixfoldAndQuarter), + pullToRefreshState = pullToRefreshState + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProfileSection( + profileData: UiState, + pullToRefreshState: PullToRefreshState, + consentState: ConsentState, + onGetConsent: (id: ProfileIdentifier) -> Unit, + onChooseAuthenticationMethod: (id: ProfileIdentifier) -> Unit, + onRefresh: () -> Unit +) { + UiStateMachine( + state = profileData, + onError = { + ErrorScreenComponent( + onClickRetry = onRefresh + ) + }, + onEmpty = { + Center { + CircularProgressIndicator() + } + }, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onContent = { activeProfile -> + val ssoTokenValid = activeProfile.isSSOTokenValid() + LaunchedEffect(activeProfile) { + if (activeProfile.isPkv()) { + ConsentValidator.validateAndExecute( + isSsoTokenValid = ssoTokenValid, + consentState = consentState, + getChargeConsent = { + onRefresh() + onGetConsent(activeProfile.id) + }, + onConsentGranted = onRefresh + ) + } else { + onRefresh() + } + } + + @Requirement( + "A_24857#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Refreshing the prescription list happens only if the user is authenticated. " + + "If the user is not authenticated, the user is prompted to authenticate." + ) + with(pullToRefreshState) { + trigger( + block = { + if (activeProfile.isPkv()) { + ConsentValidator.validateAndExecute( + isSsoTokenValid = ssoTokenValid, + consentState = consentState, + getChargeConsent = { + onRefresh() + onGetConsent(activeProfile.id) + }, + onConsentGranted = onRefresh + ) + } else { + onRefresh() + } + }, + onNavigation = { + if (!ssoTokenValid) { + endRefresh() + onChooseAuthenticationMethod(activeProfile.id) + } + } + ) + } + } + ) +} + +@Composable +private fun PrescriptionsSection( + modifier: Modifier = Modifier, + listState: LazyListState, + activeProfile: UiState, + activePrescriptions: UiState>, + isArchiveEmpty: Boolean, + onElevateTopBar: (Boolean) -> Unit, + onClickPrescription: (String) -> Unit, + onClickLogin: () -> Unit, + onClickRefresh: () -> Unit, + onClickAvatar: () -> Unit, + onClickArchive: () -> Unit +) { + LaunchedEffect(Unit) { + snapshotFlow { + listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 + }.collect { + onElevateTopBar(it) + } + } + + UiStateMachine( + state = activePrescriptions, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onError = { + ErrorScreenComponent( + onClickRetry = onClickLogin + ) + }, + onEmpty = { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag(TestTag.Prescriptions.Content), + state = listState, + contentPadding = PaddingValues(bottom = SizeDefaults.eightfoldAndThreeQuarter), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + emptyContentSection( + activeProfile = activeProfile, + onClickConnect = onClickLogin, + onClickAvatar = onClickAvatar + ) + archiveSection( + isArchiveEmpty = isArchiveEmpty, + onClickArchive = onClickArchive + ) + } + }, + onContent = { prescriptions -> + LazyColumn( + modifier = modifier + .fillMaxSize() + .testTag(TestTag.Prescriptions.Content), + state = listState, + contentPadding = PaddingValues(bottom = SizeDefaults.eightfoldAndThreeQuarter), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + profileConnectorSection( + activeProfile = activeProfile, + onClickAvatar = onClickAvatar, + onClickLogin = onClickLogin, + onClickRefresh = onClickRefresh + ) + prescriptionContentSection( + activePrescriptions = prescriptions, + onClickPrescription = onClickPrescription + ) + archiveSection( + isArchiveEmpty = isArchiveEmpty, + onClickArchive = onClickArchive + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@LightDarkPreview +@Composable +fun PrescriptionsScreenScaffoldPreview( + @PreviewParameter(PrescriptionScreenPreviewParameterProvider::class) data: PrescriptionScreenPreviewData +) { + PreviewAppTheme { + PrescriptionsScreenScaffold( + pullToRefreshState = rememberPullToRefreshState(), + listState = rememberLazyListState(), + activePrescriptions = data.activePrescription, + isArchiveEmpty = data.isArchivedEmpty, + hasRedeemableTasks = data.hasRedeemableTasks, + multiProfileData = data.multiProfileAppBarFlowWrapper, + profileData = data.profileData, + consentState = data.consentState, + isTopBarElevated = data.isTopBarElevated, + fabPadding = data.fabPadding, + prescriptionClickAction = data.prescriptionsClickAction, + topAppBarClickAction = data.topAppBarClickAction, + consentClickAction = data.consentClickAction + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreen.kt new file mode 100644 index 00000000..c8d0ac61 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreen.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UsingMaterialAndMaterial3Libraries") + +package de.gematik.ti.erp.app.prescription.ui.screen + +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.prescription.presentation.rememberWelcomeDrawerController +import de.gematik.ti.erp.app.prescription.ui.components.CommonDrawerScreenContent +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.FullScreenLoadingIndicator +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +/** + * This drawer is a one time show, once it is shown it will be hidden from the user + */ +class WelcomeDrawerBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = true) { + @Composable + override fun Content() { + val controller = rememberWelcomeDrawerController() + val profileData by controller.activeProfile.collectAsStateWithLifecycle() + + controller.onWelcomeDrawerShown() + UiStateMachine( + state = profileData, + onLoading = { + FullScreenLoadingIndicator() + }, + onError = { + ErrorScreenComponent() + } + ) { profile -> + WelcomeDrawerScreenContent( + onClickConnect = { + navController.navigate( + CardWallRoutes.CardWallIntroScreen.path(profile.id) + ) + }, + onCancel = { + navController.popBackStack() + } + ) + } + } +} + +@Composable +private fun WelcomeDrawerScreenContent( + onClickConnect: () -> Unit, + onCancel: () -> Unit +) { + CommonDrawerScreenContent( + modifierText = Modifier + .wrapContentSize() + .testTag(TestTag.Main.MainScreenBottomSheet.ConnectLaterButton), + header = stringResource(R.string.mainscreen_welcome_drawer_header), + info = stringResource(R.string.mainscreen_welcome_drawer_info), + image = painterResource(R.drawable.man_phone_blue_circle), + connectButtonText = stringResource(R.string.mainscreen_connect_bottomsheet_connect), + cancelButtonText = stringResource(R.string.mainscreen_connect_bottomsheet_connect_later), + onClickConnect = onClickConnect, + onCancel = onCancel + ) +} + +@LightDarkPreview +@Composable +fun WelcomeDrawerScreenContentPreview() { + PreviewAppTheme { + WelcomeDrawerScreenContent( + onClickConnect = {}, + onCancel = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/DeletePrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/DeletePrescriptionUseCase.kt new file mode 100644 index 00000000..b77f5d99 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/DeletePrescriptionUseCase.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.api.HTTP_BAD_REQUEST +import de.gematik.ti.erp.app.api.HTTP_FORBIDDEN +import de.gematik.ti.erp.app.api.HTTP_INTERNAL_ERROR +import de.gematik.ti.erp.app.api.HTTP_METHOD_NOT_ALLOWED +import de.gematik.ti.erp.app.api.HTTP_TOO_MANY_REQUESTS +import de.gematik.ti.erp.app.api.HTTP_UNAUTHORIZED +import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException +import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import java.net.HttpURLConnection +import java.net.UnknownHostException + +@Requirement( + "A_19229-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "User can delete a locally and remotely stored prescription and all its linked resources." +) +class DeletePrescriptionUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + sealed interface DeletePrescriptionState : ErpServiceState { + sealed interface ValidState : DeletePrescriptionState { + data object Deleted : ValidState + + data object NotDeleted : ValidState + } + + sealed interface ErrorState : DeletePrescriptionState { + data object BadRequest : ErrorState + + data object Unauthorized : ErrorState + + data object NoInternet : ErrorState + + data object ErpWorkflowBlocked : ErrorState + + data object Unknown : ErrorState + + data object MethodNotAllowed : ErrorState + + data object TooManyRequests : ErrorState + + data object InternalError : ErrorState + } + } + + suspend operator fun invoke( + profileId: ProfileIdentifier, + taskId: String, + deleteLocallyOnly: Boolean + ): Flow = + flowOf( + if (!repository.wasProfileEverAuthenticated(profileId) || deleteLocallyOnly) { + // delete local saved tasks, if sso token is null + // (profile was never connected and has imported/scanned task) + repository.deleteLocalTaskById(taskId) + repository.deleteLocalInvoicesById(taskId) + DeletePrescriptionState.ValidState.Deleted + } else { + repository + .deleteRemoteTaskById(profileId = profileId, taskId = taskId) + .fold( + onSuccess = { + repository.deleteLocalTaskById(taskId) + repository.deleteLocalInvoicesById(taskId) + DeletePrescriptionState.ValidState.Deleted + }, + onFailure = { + when (it) { + is ApiCallException -> { + when (it.response.code()) { + HttpURLConnection.HTTP_GONE, + HttpURLConnection.HTTP_NOT_FOUND + -> { + repository.deleteLocalTaskById(taskId) + DeletePrescriptionState.ValidState.Deleted + } + + else -> mapDeleteErrorStates(it) + } + } + + is RefreshFlowException -> { + DeletePrescriptionState.ErrorState.Unauthorized + } + + else -> { + mapDeleteErrorStates(it) + } + } + } + ) + } + ).catchAndTransformRemoteExceptions().flowOn(dispatcher) + + /** + * Map the error states to the corresponding [DeletePrescriptionState.ErrorState] + * Follows the specification of + * + * gemSpec_eRp_FdV + */ + private fun mapDeleteErrorStates(error: Throwable): DeletePrescriptionState.ErrorState { + if (error.cause?.cause is UnknownHostException) { + return DeletePrescriptionState.ErrorState.NoInternet + } else { + return when ((error as? ApiCallException)?.response?.code()) { + HTTP_BAD_REQUEST -> DeletePrescriptionState.ErrorState.BadRequest // 400 + HTTP_UNAUTHORIZED -> DeletePrescriptionState.ErrorState.Unauthorized // 401 + HTTP_FORBIDDEN -> DeletePrescriptionState.ErrorState.ErpWorkflowBlocked // 403 + HTTP_METHOD_NOT_ALLOWED -> DeletePrescriptionState.ErrorState.MethodNotAllowed // 405 + HTTP_TOO_MANY_REQUESTS -> DeletePrescriptionState.ErrorState.TooManyRequests // 429 + HTTP_INTERNAL_ERROR -> DeletePrescriptionState.ErrorState.InternalError // 500 + + else -> { + // silent fail + DeletePrescriptionState.ErrorState.Unknown + } + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt deleted file mode 100644 index 048cf4fc..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.usecase - -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.merge - -class GeneratePrescriptionDetailsUseCase( - private val repository: PrescriptionRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - operator fun invoke(taskId: String): Flow { - val synced = repository - .loadSyncedTaskByTaskId(taskId) - .mapNotNull { it } - .map(PrescriptionData::Synced) - .flowOn(dispatcher) - - val scanned = repository - .loadScannedTaskByTaskId(taskId) - .mapNotNull { it } - .map(PrescriptionData::Scanned) - .flowOn(dispatcher) - - // We functionally know that - return merge(synced, scanned).flowOn(dispatcher) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt index b9790417..0d48e446 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt index 3206362f..054a26a9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesDetailStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesDetailStateUseCase.kt new file mode 100644 index 00000000..58bdd894 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesDetailStateUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import de.gematik.ti.erp.app.prescription.repository.DownloadResourcesStateRepository +import kotlinx.coroutines.flow.StateFlow + +class GetDownloadResourcesDetailStateUseCase( + private val downloadResourcesStateRepository: DownloadResourcesStateRepository +) { + fun invoke(): StateFlow = downloadResourcesStateRepository.detailState() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesSnapshotStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesSnapshotStateUseCase.kt new file mode 100644 index 00000000..93847010 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetDownloadResourcesSnapshotStateUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import de.gematik.ti.erp.app.prescription.repository.DownloadResourcesStateRepository +import kotlinx.coroutines.flow.SharedFlow + +class GetDownloadResourcesSnapshotStateUseCase( + private val downloadResourcesStateRepository: DownloadResourcesStateRepository +) { + operator fun invoke(): SharedFlow = downloadResourcesStateRepository.snapshotState() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetPrescriptionByTaskIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetPrescriptionByTaskIdUseCase.kt new file mode 100644 index 00000000..179d0c3a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetPrescriptionByTaskIdUseCase.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge + +class GetPrescriptionByTaskIdUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(taskId: String): Flow { + val synced = + repository + .loadSyncedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Synced) + .flowOn(dispatcher) + + val scanned = + repository + .loadScannedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Scanned) + .flowOn(dispatcher) + + // We functionally know that + return merge(synced, scanned).flowOn(dispatcher) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt index b6a911d3..504d1f22 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt @@ -1,25 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository @@ -28,14 +27,10 @@ import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator import de.gematik.ti.erp.app.prescription.ui.ValidScannedCode import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.github.aakira.napier.Napier -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -151,24 +146,33 @@ class PrescriptionUseCase( ) } - suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) = - repository.saveScannedTasks(profileId, tasks) + suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List, + medicationString: String + ) { + repository.saveScannedTasks(profileId, tasks, medicationString) + } - suspend fun saveScannedCodes(profileId: ProfileIdentifier, scannedCodes: List) { + suspend fun saveScannedCodes( + profileId: ProfileIdentifier, + scannedCodes: List, + medicationString: String + ) { val tasks = scannedCodes.flatMap { code -> code.extract().mapIndexed { index, (_, taskId, accessCode) -> ScannedTaskData.ScannedTask( profileId = profileId, taskId = taskId, index = index, - name = null, + name = "", // name will be set later accessCode = accessCode, scannedOn = code.raw.scannedOn, redeemedOn = null ) } } - tasks.takeIf { it.isNotEmpty() }?.let { saveScannedTasks(profileId, it) } + tasks.takeIf { it.isNotEmpty() }?.let { saveScannedTasks(profileId, it, medicationString) } } private fun ValidScannedCode.extract(): List> = @@ -185,32 +189,6 @@ class PrescriptionUseCase( suspend fun downloadTasks(profileId: ProfileIdentifier): Result = taskRepository.downloadTasks(profileId) - @OptIn(ExperimentalCoroutinesApi::class) - suspend fun generatePrescriptionDetails( - taskId: String - ): Flow = - repository.loadSyncedTaskByTaskId(taskId).transformLatest { task -> - if (task == null) { - repository.loadScannedTaskByTaskId(taskId).collectLatest { scannedTask -> - if (scannedTask == null) { - Napier.w("No task `$taskId` found!") - } else { - emit(PrescriptionData.Scanned(task = scannedTask)) - } - } - } else { - emit(PrescriptionData.Synced(task = task)) - } - }.flowOn(dispatchers.io) - - suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result { - return repository.deleteTaskByTaskId(profileId, taskId) - } - - suspend fun redeemScannedTask(taskId: String, redeem: Boolean) { - repository.updateRedeemedOn(taskId, if (redeem) Clock.System.now() else null) - } - fun getAllTasksWithTaskIdOnly(): Flow> = repository.loadTaskIds() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RedeemScannedTaskUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RedeemScannedTaskUseCase.kt new file mode 100644 index 00000000..0f03548f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RedeemScannedTaskUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock + +class RedeemScannedTaskUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + taskId: String, + redeem: Boolean + ) { + withContext(dispatcher) { + repository.updateRedeemedOn(taskId, if (redeem) Clock.System.now() else null) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt index 0a923498..2eba128d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository import de.gematik.ti.erp.app.prescription.repository.TaskRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.repository.ProfileRepository @@ -36,6 +36,10 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +enum class RefreshState { + NotStarted, InProgress, Finished +} + class RefreshPrescriptionUseCase( private val repository: TaskRepository, private val communicationRepository: CommunicationRepository, @@ -53,14 +57,14 @@ class RefreshPrescriptionUseCase( private val requestChannel = Channel(onUndeliveredElement = { it.resultChannel.close(CancellationException()) }) - private val _refreshInProgress = MutableStateFlow(false) - val refreshInProgress: StateFlow + private val _refreshInProgress = MutableStateFlow(RefreshState.NotStarted) + val refreshInProgress: StateFlow get() = _refreshInProgress init { scope.launch { for (request in requestChannel) { - _refreshInProgress.value = true + _refreshInProgress.value = RefreshState.InProgress Napier.d { "Start refreshing as per request" } val profileId = request.forProfileId @@ -71,6 +75,7 @@ class RefreshPrescriptionUseCase( if (profilesRepository.checkIsProfilePKV(profileId)) { invoiceRepository.downloadInvoices(profileId) } + // downloadResourcesStateRepository.updateState(nrOfNewPrescriptions) nrOfNewPrescriptions } @@ -78,15 +83,20 @@ class RefreshPrescriptionUseCase( request.resultChannel.trySend(result) Napier.d { "Finished refreshing" } - _refreshInProgress.value = false + _refreshInProgress.value = RefreshState.Finished } } } - suspend fun download(profileId: ProfileIdentifier): Result { + private suspend fun download(profileId: ProfileIdentifier): Result { val resultChannel = Channel>() try { - requestChannel.send(Request(resultChannel = resultChannel, forProfileId = profileId)) + requestChannel.send( + Request( + resultChannel = resultChannel, + forProfileId = profileId + ) + ) return resultChannel.receive() } catch (cancellation: CancellationException) { Napier.d { "Cancelled waiting for result of refresh request" } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/SaveWelcomeMessageUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/SaveWelcomeMessageUseCase.kt new file mode 100644 index 00000000..73e222c5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/SaveWelcomeMessageUseCase.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository + +class SaveWelcomeMessageUseCase( + private val inAppMessageRepository: InAppMessageRepository +) { + suspend operator fun invoke() { + inAppMessageRepository.setShowWelcomeMessage() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt index fe50e3ae..7895ebdc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase @@ -30,7 +30,9 @@ class UpdateScannedTaskNameUseCase( suspend operator fun invoke( taskId: String, name: String - ) = withContext(dispatcher) { - repository.updateScannedTaskName(taskId, name) + ) { + withContext(dispatcher) { + repository.updateScannedTaskName(taskId, name) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt index 3864f0b4..38ed49f7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase.model @@ -21,9 +21,15 @@ package de.gematik.ti.erp.app.prescription.usecase.model import androidx.compose.runtime.Immutable import de.gematik.ti.erp.app.prescription.model.Communication import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable @Immutable +@Serializable sealed interface Prescription { val name: String? val taskId: String @@ -31,10 +37,20 @@ sealed interface Prescription { val startedOn: Instant? val expiresOn: Instant? + companion object { + suspend fun StateFlow>>.countByType(type: Class): Int? { + return first { it.isDataState }.data + ?.filterIsInstance(type) + ?.takeIf { it.isNotEmpty() } + ?.count() + } + } + /** * Represents a single [Task] synchronized with the backend. */ @Immutable + @Serializable data class SyncedPrescription( override val taskId: String, override val name: String?, @@ -51,7 +67,9 @@ sealed interface Prescription { override val startedOn = authoredOn } + @Serializable data class PrescriptionChipInformation( + val isSelfPayPrescription: Boolean = false, val isPartOfMultiplePrescription: Boolean = false, val numerator: String? = null, val denominator: String? = null, @@ -62,9 +80,10 @@ sealed interface Prescription { * Represents a single [Task] scanned by the user. */ @Immutable + @Serializable data class ScannedPrescription( override val taskId: String, - override val name: String?, + override val name: String, override val redeemedOn: Instant?, val scannedOn: Instant, val index: Int, @@ -76,10 +95,7 @@ sealed interface Prescription { fun redeemedOrExpiredOn(): Instant = when (this) { - is ScannedPrescription -> requireNotNull(redeemedOn) { - "Scanned prescriptions require " + - "a redeemed timestamp" - } + is ScannedPrescription -> requireNotNull(redeemedOn) { "Scanned prescription needs a redeemed timestamp" } is SyncedPrescription -> redeemedOn ?: expiresOn ?: authoredOn } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt index 0f24a6ae..b89bb966 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.usecase.model diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt index 3e67b115..eb764934 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles @@ -22,15 +22,18 @@ import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.DecryptAccessTokenUseCase +import de.gematik.ti.erp.app.profiles.usecase.DeletePairedDevicesUseCase import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetPairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase import de.gematik.ti.erp.app.profiles.usecase.GetSelectedProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.IsProfilePKVUseCase import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase -import de.gematik.ti.erp.app.profiles.usecase.ResetProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToGKVUseCase import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase import org.kodein.di.DI @@ -41,16 +44,18 @@ val profilesModule = DI.Module("profilesModule") { bindProvider { AddProfileUseCase(instance()) } bindProvider { DeleteProfileUseCase(instance(), instance()) } bindProvider { GetActiveProfileUseCase(instance()) } + bindProvider { GetProfileByIdUseCase(instance()) } bindProvider { GetProfilesUseCase(instance()) } - bindProvider { ResetProfileUseCase(instance(), instance()) } bindProvider { SwitchActiveProfileUseCase(instance()) } bindProvider { UpdateProfileUseCase(instance()) } bindProvider { DecryptAccessTokenUseCase(instance()) } bindProvider { LogoutProfileUseCase(instance()) } bindProvider { SwitchProfileToPKVUseCase(instance()) } + bindProvider { SwitchProfileToGKVUseCase(instance()) } + bindProvider { IsProfilePKVUseCase(instance()) } bindProvider { GetSelectedProfileUseCase(instance()) } - - bindProvider { ProfilesWithPairedDevicesUseCase(instance()) } + bindProvider { GetPairedDevicesUseCase(instance()) } + bindProvider { DeletePairedDevicesUseCase(instance()) } bindProvider { ProfilesUseCase(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/PictureDataType.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/PictureDataType.kt new file mode 100644 index 00000000..bbc30990 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/PictureDataType.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.model + +internal enum class PictureDataType { + EMPTY, + IMAGE, + TEXT +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfileCombinedData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfileCombinedData.kt new file mode 100644 index 00000000..78aeec62 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfileCombinedData.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.model + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +@Immutable +data class ProfileCombinedData( + @Stable val selectedProfile: ProfilesUseCaseData.Profile?, + @Stable val profiles: List = emptyList() +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilePairedDevicesErrorState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilePairedDevicesErrorState.kt new file mode 100644 index 00000000..b769d1dc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilePairedDevicesErrorState.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.model + +sealed class ProfilePairedDevicesErrorState : Throwable() { + data object UserNotLoggedInWithBiometricsError : ProfilePairedDevicesErrorState() + data object CannotLoadPairedDevicesError : ProfilePairedDevicesErrorState() + data object NoInternetError : ProfilePairedDevicesErrorState() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileGraph.kt index 7f7b7fe6..c286684c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.navigation @@ -21,13 +21,19 @@ package de.gematik.ti.erp.app.profiles.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderBottomSheet import de.gematik.ti.erp.app.navigation.renderComposable -import de.gematik.ti.erp.app.profiles.ui.ProfileAuditEventsScreen -import de.gematik.ti.erp.app.profiles.ui.ProfileEditPictureScreen -import de.gematik.ti.erp.app.profiles.ui.ProfileImageCropperScreen -import de.gematik.ti.erp.app.profiles.ui.ProfilePairedDevicesScreen -import de.gematik.ti.erp.app.profiles.ui.ProfileScreen -import de.gematik.ti.erp.app.profiles.ui.ProfileTokenScreen +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideOutUp +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileAuditEventsScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileEditNameBottomSheetScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileEditPictureBottomSheetScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileEditPictureScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileImageCameraScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileImageCropperScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileImageEmojiScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfilePairedDevicesScreen +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileScreen fun NavGraphBuilder.profileGraph( startDestination: String = ProfileRoutes.ProfileScreen.route, @@ -38,6 +44,9 @@ fun NavGraphBuilder.profileGraph( route = ProfileRoutes.subGraphName() ) { renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, route = ProfileRoutes.ProfileScreen.route, arguments = ProfileRoutes.ProfileScreen.arguments ) { navEntry -> @@ -55,6 +64,33 @@ fun NavGraphBuilder.profileGraph( navBackStackEntry = navEntry ) } + renderBottomSheet( + route = ProfileRoutes.ProfileEditPictureBottomSheetScreen.route, + arguments = ProfileRoutes.ProfileEditPictureBottomSheetScreen.arguments + ) { navEntry -> + ProfileEditPictureBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = ProfileRoutes.ProfileEditNameBottomSheetScreen.route, + arguments = ProfileRoutes.ProfileEditNameBottomSheetScreen.arguments + ) { navEntry -> + ProfileEditNameBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderBottomSheet( + route = ProfileRoutes.ProfileAddNameBottomSheetScreen.route, + arguments = ProfileRoutes.ProfileAddNameBottomSheetScreen.arguments + ) { navEntry -> + ProfileEditNameBottomSheetScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } renderComposable( route = ProfileRoutes.ProfileImageCropperScreen.route, arguments = ProfileRoutes.ProfileImageCropperScreen.arguments @@ -65,10 +101,19 @@ fun NavGraphBuilder.profileGraph( ) } renderComposable( - route = ProfileRoutes.ProfileTokenScreen.route, - arguments = ProfileRoutes.ProfileTokenScreen.arguments + route = ProfileRoutes.ProfileImageEmojiScreen.route, + arguments = ProfileRoutes.ProfileImageEmojiScreen.arguments + ) { navEntry -> + ProfileImageEmojiScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + renderComposable( + route = ProfileRoutes.ProfileImageCameraScreen.route, + arguments = ProfileRoutes.ProfileImageCameraScreen.arguments ) { navEntry -> - ProfileTokenScreen( + ProfileImageCameraScreen( navController = navController, navBackStackEntry = navEntry ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileRoutes.kt index df5b993e..78c4c0e4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/navigation/ProfileRoutes.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.navigation +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavType import androidx.navigation.navArgument import de.gematik.ti.erp.app.navigation.NavigationRouteNames @@ -26,41 +27,75 @@ import de.gematik.ti.erp.app.navigation.Routes object ProfileRoutes : NavigationRoutes { override fun subGraphName() = "profile" - const val ProfileId = "profileId" + const val PROFILE_NAV_PROFILE_ID = "PROFILE_ID_PROFILE_NAVIGATION" + object ProfileScreen : Routes( NavigationRouteNames.ProfileScreen.name, - navArgument(ProfileId) { type = NavType.StringType } - ) { - fun path(profileId: String) = path(ProfileId to profileId) - } - object ProfileTokenScreen : Routes( - NavigationRouteNames.ProfileTokenScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) } + object ProfileAuditEventsScreen : Routes( NavigationRouteNames.ProfileAuditEventsScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) } + object ProfilePairedDevicesScreen : Routes( NavigationRouteNames.ProfilePairedDevicesScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) } + object ProfileEditPictureScreen : Routes( NavigationRouteNames.ProfileEditPictureScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) } + object ProfileImageCropperScreen : Routes( NavigationRouteNames.ProfileImageCropperScreen.name, - navArgument(ProfileId) { type = NavType.StringType } + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } ) { - fun path(profileId: String) = path(ProfileId to profileId) + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) } + + object ProfileImageEmojiScreen : Routes( + NavigationRouteNames.ProfileImageEmojiScreen.name, + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } + ) { + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) + } + + object ProfileImageCameraScreen : Routes( + NavigationRouteNames.ProfileImageCameraScreen.name, + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } + ) { + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) + } + + object ProfileEditPictureBottomSheetScreen : Routes( + NavigationRouteNames.ProfileEditPictureBottomSheetScreen.name, + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } + ) { + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) + } + + object ProfileEditNameBottomSheetScreen : Routes( + NavigationRouteNames.ProfileEditNameBottomSheetScreen.name, + navArgument(PROFILE_NAV_PROFILE_ID) { type = NavType.StringType } + ) { + fun path(profileId: String) = path(PROFILE_NAV_PROFILE_ID to profileId) + } + + object ProfileAddNameBottomSheetScreen : Routes( + NavigationRouteNames.ProfileAddNameBottomSheetScreen.name + ) + + fun getProfileId(entry: NavBackStackEntry): String = + requireNotNull(entry.arguments?.getString(PROFILE_NAV_PROFILE_ID)) { "profile-id cannot be null for this navigation" } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/AuditEventsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/AuditEventsController.kt index 462f6e2c..5f75294f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/AuditEventsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/AuditEventsController.kt @@ -1,153 +1,123 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.paging.PagingData -import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator -import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.prescription.presentation.catchAndTransformRemoteExceptions -import de.gematik.ti.erp.app.prescription.presentation.retryWithAuthenticator +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.ChooseAuthenticationController +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase import de.gematik.ti.erp.app.protocol.usecase.AuditEventsUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import org.kodein.di.compose.rememberInstance +@Requirement( + "A_19177#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "The audit events are displayed in a paged manner." +) @Stable class AuditEventsController( - private val auditEventsUseCase: AuditEventsUseCase, - private val authenticator: Authenticator, - coroutineScope: CoroutineScope + selectedProfileId: ProfileIdentifier, + getProfileByIdUseCase: GetProfileByIdUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + getProfilesUseCase: GetProfilesUseCase, + chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + authenticator: BiometricAuthenticator, + private val auditEventsUseCase: AuditEventsUseCase +) : ChooseAuthenticationController( + profileId = selectedProfileId, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = authenticator ) { + val showAuthenticationErrorDialog = ComposableEvent() + val refreshStartedEvent = ComposableEvent() - private val searchChannelFlow = MutableStateFlow("") + private val refreshTrigger = MutableStateFlow(false) - var profileId by mutableStateOf("") - private set - - @Stable - sealed interface State : PrescriptionServiceState { - @Stable - object Loading : State - - @Stable - data class AuditEvents(val auditEvents: List) : State - } - - private var isLoading by mutableStateOf(false) - - suspend fun refresh( - profileId: ProfileIdentifier, - isUserAction: Boolean, - onUserNotAuthenticated: () -> Unit, - onShowCardWall: () -> Unit - ) { - val finalState = refreshFlow( - profileId = profileId, - isUserAction = isUserAction - ).cancellable().first() - - when (finalState) { - GeneralErrorState.NoneEnrolled -> { - onShowCardWall() - } - GeneralErrorState.UserNotAuthenticated -> { - onUserNotAuthenticated() - } - else -> { - } - } + init { + biometricAuthenticationSuccessEvent.listen(controllerScope) { refreshAuditEvents() } + biometricAuthenticationResetErrorEvent.listen(controllerScope) { showAuthenticationErrorDialog.trigger(it) } + biometricAuthenticationOtherErrorEvent.listen(controllerScope) { showAuthenticationErrorDialog.trigger(it) } } @OptIn(ExperimentalCoroutinesApi::class) - val auditEventPagingFlow: Flow> = - searchChannelFlow - .filterNotNull() - .onEach { - profileId = it - } - .flatMapLatest { searchData -> - isLoading = true - - auditEventsUseCase.loadAuditEventsPaged(searchData) - .onEach { - isLoading = false + val auditEvents by lazy { + refreshTrigger + .flatMapLatest { + auditEventsUseCase.invoke(selectedProfileId) + .onEach { refreshStartedEvent.trigger() } + .onCompletion { + refreshCombinedProfile() } - .flowOn(Dispatchers.IO) .shareIn( - coroutineScope, - SharingStarted.WhileSubscribed(), - 1 + scope = controllerScope, + started = SharingStarted.WhileSubscribed(), + replay = 1 ) } + } - private fun refreshFlow( - profileId: ProfileIdentifier, - isUserAction: Boolean - ): Flow = - flow { - searchChannelFlow.emit(profileId) - emit(State.Loading) - val auditEvents = auditEventsUseCase.loadAuditEvents(profileId) - emit(State.AuditEvents(auditEvents)) - } - .retryWithAuthenticator( - isUserAction = isUserAction, - authenticate = authenticator.authenticateForPrescriptions(profileId) - ) - .catchAndTransformRemoteExceptions() - .flowOn(Dispatchers.IO) + fun refreshAuditEvents() { + refreshTrigger.value = !refreshTrigger.value + } } @Composable -fun rememberAuditEventsController(): AuditEventsController { +fun rememberAuditEventsController( + profileId: ProfileIdentifier +): AuditEventsController { + val getProfilesUseCase by rememberInstance() + val getProfileByIdUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() val auditEventsUseCase by rememberInstance() - val authenticator = LocalAuthenticator.current - val scope = rememberCoroutineScope() + val chooseAuthenticationDataUseCase by rememberInstance() + val authenticator = LocalBiometricAuthenticator.current return remember { AuditEventsController( + selectedProfileId = profileId, + getProfilesUseCase = getProfilesUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, auditEventsUseCase = auditEventsUseCase, - authenticator = authenticator, - coroutineScope = scope + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + authenticator = authenticator ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt index d8b31e2c..f714330a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("TooManyFunctions") package de.gematik.ti.erp.app.profiles.presentation @@ -22,136 +23,82 @@ package de.gematik.ti.erp.app.profiles.presentation import android.graphics.Bitmap import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.Controller import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase -import de.gematik.ti.erp.app.profiles.usecase.DecryptAccessTokenUseCase -import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase -import de.gematik.ti.erp.app.profiles.usecase.ResetProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase -import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier -import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance class ProfileController( - private val addProfileUseCase: AddProfileUseCase, - private val decryptAccessTokenUseCase: DecryptAccessTokenUseCase, - private val deleteProfileUseCase: DeleteProfileUseCase, private val getActiveProfileUseCase: GetActiveProfileUseCase, private val getProfilesUseCase: GetProfilesUseCase, - private val resetProfileUseCase: ResetProfileUseCase, private val switchActiveProfileUseCase: SwitchActiveProfileUseCase, - private val updateProfileUseCase: UpdateProfileUseCase, - private val logoutProfileUseCase: LogoutProfileUseCase, - private val switchProfileToPKVUseCase: SwitchProfileToPKVUseCase, - private val profilesWithPairedDevicesUseCase: ProfilesWithPairedDevicesUseCase, - private val scope: CoroutineScope -) { - - private val profiles by lazy { - getProfilesUseCase().stateIn(scope, SharingStarted.Lazily, listOf(DEFAULT_EMPTY_PROFILE)) - } + private val updateProfileUseCase: UpdateProfileUseCase +) : Controller() { - private val activeProfile by lazy { - getActiveProfileUseCase().stateIn(scope, SharingStarted.Lazily, DEFAULT_EMPTY_PROFILE) - } - - @Composable - fun decryptedAccessToken(profile: Profile) = - decryptAccessTokenUseCase(profile.id).collectAsStateWithLifecycle(null) - - fun pairedDevices(profileId: ProfileIdentifier) = - profilesWithPairedDevicesUseCase.pairedDevices(profileId) - - suspend fun deletePairedDevice(profileId: ProfileIdentifier, device: PairedDevice) = - profilesWithPairedDevicesUseCase.deletePairedDevices(profileId, device) - - @Requirement( - "O.Tokn_6#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate config and token " - ) - fun logout(profile: Profile) { - scope.launch { - logoutProfileUseCase(profile.id) - } - } + private val _profile: MutableStateFlow = MutableStateFlow(null) + val profile: StateFlow = _profile - fun addProfile(profileName: String) { - scope.launch { - addProfileUseCase(profileName) - } + private val _profiles by lazy { + getProfilesUseCase().stateIn(controllerScope, SharingStarted.Eagerly, null) } - fun removeProfile(profile: Profile, profileName: String?) { - scope.launch { - when (profileName != null) { - true -> resetProfileUseCase(profile, profileName) - false -> deleteProfileUseCase(profile) - } - } + private val activeProfile by lazy { + getActiveProfileUseCase().stateIn(controllerScope, SharingStarted.Lazily, DEFAULT_EMPTY_PROFILE) } + // todo: replace getProfilesState with getProfilesState2 @Composable - fun getProfilesState() = profiles.collectAsStateWithLifecycle() + fun getProfilesState2() = _profiles.collectAsStateWithLifecycle() @Composable fun getActiveProfileState() = activeProfile.collectAsStateWithLifecycle() fun switchActiveProfile(id: ProfileIdentifier) { - scope.launch { + controllerScope.launch { switchActiveProfileUseCase(id) } } - fun switchToPrivateInsurance(profileId: ProfileIdentifier) { - scope.launch { - switchProfileToPKVUseCase(profileId) - } - } - fun updateProfileColor(profile: Profile, color: ProfilesData.ProfileColorNames) { - scope.launch { + controllerScope.launch { updateProfileUseCase(modifier = ProfileModifier.Color(color), id = profile.id) } } fun savePersonalizedProfileImage(profileId: ProfileIdentifier, image: Bitmap) { - scope.launch { + controllerScope.launch { updateProfileUseCase(modifier = ProfileModifier.Image(image), id = profileId) } } fun updateProfileName(profileId: ProfileIdentifier, name: String) { - scope.launch { + controllerScope.launch { updateProfileUseCase(modifier = ProfileModifier.Name(name), id = profileId) } } fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { - scope.launch { + controllerScope.launch { updateProfileUseCase(modifier = ProfileModifier.Avatar(avatar), id = profileId) } } fun clearPersonalizedImage(profileId: ProfileIdentifier) { - scope.launch { + controllerScope.launch { updateProfileUseCase(modifier = ProfileModifier.ClearImage, id = profileId) } } @@ -163,7 +110,7 @@ class ProfileController( insurance = ProfileInsuranceInformation( insuranceType = ProfilesUseCaseData.InsuranceType.NONE ), - active = false, + isActive = false, color = ProfilesData.ProfileColorNames.SPRING_GRAY, lastAuthenticated = null, ssoTokenScope = null, @@ -174,33 +121,17 @@ class ProfileController( @Composable fun rememberProfileController(): ProfileController { - val addProfileUseCase by rememberInstance() - val decryptAccessTokenUseCase by rememberInstance() - val deleteProfileUseCase by rememberInstance() val getActiveProfileUseCase by rememberInstance() val getProfilesUseCase by rememberInstance() - val resetProfileUseCase by rememberInstance() val switchActiveProfileUseCase by rememberInstance() val updateProfileUseCase by rememberInstance() - val logoutProfileUseCase by rememberInstance() - val switchProfileToPKVUseCase by rememberInstance() - val profilesWithPairedDevicesUseCase by rememberInstance() - val scope = rememberCoroutineScope() return remember { ProfileController( - addProfileUseCase = addProfileUseCase, - decryptAccessTokenUseCase = decryptAccessTokenUseCase, - deleteProfileUseCase = deleteProfileUseCase, getActiveProfileUseCase = getActiveProfileUseCase, getProfilesUseCase = getProfilesUseCase, - resetProfileUseCase = resetProfileUseCase, switchActiveProfileUseCase = switchActiveProfileUseCase, - updateProfileUseCase = updateProfileUseCase, - logoutProfileUseCase = logoutProfileUseCase, - switchProfileToPKVUseCase = switchProfileToPKVUseCase, - profilesWithPairedDevicesUseCase = profilesWithPairedDevicesUseCase, - scope = scope + updateProfileUseCase = updateProfileUseCase ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditNameController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditNameController.kt new file mode 100644 index 00000000..330a902c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditNameController.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.profiles.model.ProfileCombinedData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetSelectedProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class ProfileEditNameController( + private val profileId: ProfileIdentifier?, + private val getSelectedProfileUseCase: GetSelectedProfileUseCase, + private val getProfilesUseCase: GetProfilesUseCase, + private val updateProfileUseCase: UpdateProfileUseCase, + private val addProfileUseCase: AddProfileUseCase +) : Controller() { + + private val _combinedProfile = MutableStateFlow>(UiState.Loading()) + val combinedProfile: StateFlow> = _combinedProfile + + init { + controllerScope.launch { load() } + } + + private suspend fun load() { + runCatching { + val profiles = getProfilesUseCase().firstOrNull() + val selectedProfile = profileId?.let { + getSelectedProfileUseCase(profileId).firstOrNull() + } + selectedProfile to profiles + }.fold( + onSuccess = { (selectedProfile, profiles) -> + _combinedProfile.value = UiState.Data( + ProfileCombinedData( + selectedProfile = selectedProfile, + profiles = profiles ?: emptyList() + ) + ) + }, + onFailure = { + _combinedProfile.value = UiState.Error(it) + } + ) + } + + fun updateProfileName(name: String) { + controllerScope.launch { + _combinedProfile.value.data?.selectedProfile?.let { + updateProfileUseCase( + modifier = UpdateProfileUseCase.Companion.ProfileModifier.Name(name), + id = it.id + ) + } + } + } + + fun addNewProfile(name: String) { + controllerScope.launch { + addProfileUseCase(name) + } + } +} + +@Composable +fun rememberProfileEditNameController(profileId: ProfileIdentifier?): ProfileEditNameController { + val getSelectedProfileUseCase by rememberInstance() + val updateProfileUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val addProfileUseCase by rememberInstance() + + return remember(profileId) { + ProfileEditNameController( + profileId = profileId, + getSelectedProfileUseCase = getSelectedProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + updateProfileUseCase = updateProfileUseCase, + addProfileUseCase = addProfileUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditPictureController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditPictureController.kt new file mode 100644 index 00000000..6fc79c0b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileEditPictureController.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetSelectedProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class ProfileEditPictureController( + private val profileId: ProfileIdentifier?, + private val getSelectedProfileUseCase: GetSelectedProfileUseCase, + private val updateProfileUseCase: UpdateProfileUseCase +) : Controller() { + + private val _profile = MutableStateFlow>(UiState.Loading()) + val profile: StateFlow> = _profile + + init { + loadSelectedProfile(profileId) + } + + private fun loadSelectedProfile(profileId: ProfileIdentifier?) { + controllerScope.launch { + runCatching { + profileId?.let { + getSelectedProfileUseCase(profileId).first() + } ?: run { + throw IllegalArgumentException("ProfileId is null") + } + }.fold( + onSuccess = { + _profile.value = UiState.Data(it) + }, + onFailure = { + UiState.Error(it) + } + ) + } + } + + fun updateProfileColor(color: ProfilesData.ProfileColorNames) { + controllerScope.launch { + profile.value.data?.let { + updateProfileUseCase( + modifier = ProfileModifier.Color(color), + id = it.id + ) + } + } + } + + fun updateAvatar(avatar: ProfilesData.Avatar) { + controllerScope.launch { + profile.value.data?.let { + updateProfileUseCase( + modifier = ProfileModifier.Avatar(avatar), + id = it.id + ) + } + } + } + + fun clearPersonalizedImage() { + controllerScope.launch { + profile.value.data?.let { + updateProfileUseCase( + modifier = ProfileModifier.ClearImage, + id = it.id + ) + } + } + } +} + +@Composable +fun rememberProfileEditPictureController(profileId: ProfileIdentifier?): ProfileEditPictureController { + val getSelectedProfileUseCase by rememberInstance() + val updateProfileUseCase by rememberInstance() + + return remember(profileId) { + ProfileEditPictureController( + profileId = profileId, + getSelectedProfileUseCase = getSelectedProfileUseCase, + updateProfileUseCase = updateProfileUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileImagePersonalizedImageScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileImagePersonalizedImageScreenController.kt new file mode 100644 index 00000000..d6ba2e18 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileImagePersonalizedImageScreenController.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import android.graphics.Bitmap +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.model.ProfilesData.Avatar.PersonalizedImage +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Avatar +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Color +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Image +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +/** + * Controller for [ProfileImageCameraScreen] and [ProfileImageEmojiScreen] + * If those screens become bigger, then we can split this + */ +class ProfileImagePersonalizedImageScreenController( + private val profileId: ProfileIdentifier, + private val updateProfileUseCase: UpdateProfileUseCase, + private val getProfilesUseCase: GetProfilesUseCase +) : Controller() { + + private val _profile: MutableStateFlow = MutableStateFlow(null) + val profile: StateFlow = _profile + + init { + controllerScope.launch { + getProfilesUseCase().firstOrNull()?.let { + _profile.value = it.firstOrNull { profile -> profile.id == profileId } + } + } + } + + fun updateProfileImageBitmap(bitmap: Bitmap) { + controllerScope.launch { + updateProfileUseCase(modifier = Image(bitmap), id = profileId) + updateProfileUseCase(modifier = Avatar(value = PersonalizedImage), id = profileId) + } + } + + fun updateProfileColor(color: ProfilesData.ProfileColorNames) { + controllerScope.launch { + _profile.value = _profile.value?.copy(color = color) + updateProfileUseCase(modifier = Color(color), id = profileId) + } + } + + // need this as memojis are not supported on Samsung devices + fun isSamsungDevice(): Boolean { + return Build.MANUFACTURER.equals("Samsung", ignoreCase = true) + } +} + +@Composable +fun rememberProfileImagePersonalizedImageScreenController( + profileId: ProfileIdentifier +): ProfileImagePersonalizedImageScreenController { + val updateProfileUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + return remember { + ProfileImagePersonalizedImageScreenController( + profileId = profileId, + updateProfileUseCase = updateProfileUseCase, + getProfilesUseCase = getProfilesUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfilePairedDevicesScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfilePairedDevicesScreenController.kt new file mode 100644 index 00000000..8289f1c8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfilePairedDevicesScreenController.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.api.NoInternetException +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.ChooseAuthenticationController +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.NetworkStatusTracker +import de.gematik.ti.erp.app.base.NetworkStatusTracker.Companion.isNetworkAvailable +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException.Companion.isUserActionNotRequired +import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException.Companion.isUserActionRequired +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.CannotLoadPairedDevicesError +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.NoInternetError +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.UserNotLoggedInWithBiometricsError +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.DeletePairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetPairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.retry +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +private const val ASK_FOR_AUTH_REQUIRED_ATTEMPTS = 1L + +@Stable +class ProfilePairedDevicesScreenController( + private val profileId: ProfileIdentifier, + getProfileByIdUseCase: GetProfileByIdUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + getProfilesUseCase: GetProfilesUseCase, + chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + biometricAuthenticator: BiometricAuthenticator, + private val getPairedDevicesUseCase: GetPairedDevicesUseCase, + private val deletePairedDevicesUseCase: DeletePairedDevicesUseCase, + private val refreshTrigger: MutableSharedFlow = MutableSharedFlow(), + private val networkStatusTracker: NetworkStatusTracker +) : ChooseAuthenticationController( + profileId = profileId, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator, + onSelectedProfileSuccess = { profile, coroutineScope -> + coroutineScope.launch { + refreshTrigger.emit(profile) + } + } +) { + init { + observePairedDevices() + + biometricAuthenticationSuccessEvent.listen(controllerScope) { refreshPairedDevices() } + + biometricAuthenticationResetErrorEvent.listen(controllerScope) { error -> + _pairedDevices.value = UiState.Error>(CannotLoadPairedDevicesError) + showAuthenticationErrorDialog.trigger(error) + } + + biometricAuthenticationOtherErrorEvent.listen(controllerScope) { error -> + _pairedDevices.value = UiState.Error>(CannotLoadPairedDevicesError) + showAuthenticationErrorDialog.trigger(error) + } + } + + private val isNotBiometricAuthentication = MutableStateFlow(false) + private val _pairedDevices: MutableStateFlow>> = MutableStateFlow(UiState.Loading()) + + val showAuthenticationErrorDialog = ComposableEvent() + + val pairedDevices: StateFlow>> = _pairedDevices.asStateFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + private fun observePairedDevices() { + controllerScope.launch { + refreshTrigger + .onEach { UiState.Loading>() } + .onBiometricAuthentication() + .flatMapLatest { biometricProfile -> + if (biometricProfile != null) { + isNotBiometricAuthentication.value = false + // If profile is not null, continue with the flow since user has used biometric authentication + flowOf(biometricProfile) + .withUniqueToken() + .loadPairedDevices() + .retry(ASK_FOR_AUTH_REQUIRED_ATTEMPTS) { throwable -> + if (throwable.isUserActionRequired()) { // to ask for biometric authentication + Napier.i { "need bio-auth for paired devices, ${throwable.message}" } + chooseAuthenticationMethod( + profileId = profileId, + useBiometricPairingScope = true + ) + true + } else { + Napier.e { "error loading paired devices ${throwable.message}" } + UiState.Error>(CannotLoadPairedDevicesError) + false + } + } + .map { UiState.Data(it) } + } else { + // since the code will not go into the catch loop, we need to check for internet failure here + checkNetworkBeforeBiometricError() + } + } + .catch { exception -> + Napier.e { "error on loading paired devices ${exception.stackTraceToString()}" } + if (exception.isUserActionNotRequired()) { // currently asking user for biometric authentication + _pairedDevices.value = when (exception) { + is NoInternetException -> UiState.Error>(NoInternetError) + else -> UiState.Error>(CannotLoadPairedDevicesError) + } + } + }.collectLatest { + Napier.d { "collected state $it" } + _pairedDevices.value = it + } + } + } + + fun refreshPairedDevices() { + controllerScope.launch { + observePairedDevices() + refreshCombinedProfile() // refreshing the profiles triggers the onSuccess lambda inside which we have the refreshTrigger that emits the profile + } + } + + fun deletePairedDevice(device: PairedDevice) { + controllerScope.launch { + _pairedDevices.value = UiState.Loading() + deletePairedDevicesUseCase.invoke(profileId, device) + .onSuccess { _ -> + refreshPairedDevices() + } + } + } + + // the errors thrown here are to be caught in the catch block + private fun checkNetworkBeforeBiometricError(): Flow>> = + if (networkStatusTracker.isNetworkAvailable()) { + isNotBiometricAuthentication.value = true + flowOf(UiState.Error(UserNotLoggedInWithBiometricsError)) + } else { + flowOf(UiState.Error(NoInternetError)) + } + + private fun Flow.withUniqueToken(): Flow = + map { profile -> profile.ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope } + .filterNotNull().distinctUntilChanged() + + @OptIn(ExperimentalCoroutinesApi::class) + private fun Flow.loadPairedDevices(): Flow> = + flatMapLatest { token -> + getPairedDevicesUseCase.invoke( + profileId = profileId, + keyStoreAlias = token.aliasOfSecureElementEntryBase64() + ) + } +} + +@Composable +fun rememberProfilePairedDevicesScreenController( + profileId: ProfileIdentifier +): ProfilePairedDevicesScreenController { + val getProfileByIdUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val chooseAuthenticationDataUseCase by rememberInstance() + val getPairedDevicesUseCase by rememberInstance() + val deletePairedDevicesUseCase by rememberInstance() + val networkStatusTracker by rememberInstance() + val biometricAuthenticator = LocalBiometricAuthenticator.current + + return remember(profileId) { + ProfilePairedDevicesScreenController( + profileId = profileId, + getProfilesUseCase = getProfilesUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator, + getPairedDevicesUseCase = getPairedDevicesUseCase, + deletePairedDevicesUseCase = deletePairedDevicesUseCase, + networkStatusTracker = networkStatusTracker + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenController.kt new file mode 100644 index 00000000..ec3bf4f0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenController.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.presentation.GetProfileByIdController +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Suppress("LongParameterList") +class ProfileScreenController( + profileId: ProfileIdentifier, + getProfileByIdUseCase: GetProfileByIdUseCase, + getProfilesUseCase: GetProfilesUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + private val switchActiveProfileUseCase: SwitchActiveProfileUseCase, + private val updateProfileUseCase: UpdateProfileUseCase, + private val logoutProfileUseCase: LogoutProfileUseCase, + private val addProfileUseCase: AddProfileUseCase, + private val deleteProfileUseCase: DeleteProfileUseCase +) : GetProfileByIdController( + selectedProfileId = profileId, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + getActiveProfileUseCase = getActiveProfileUseCase +) { + + fun switchActiveProfile(id: ProfileIdentifier) { + controllerScope.launch { + switchActiveProfileUseCase(id) + } + } + + fun logout(profileId: ProfileIdentifier) { + controllerScope.launch { + logoutProfileUseCase(profileId) + } + } + + fun updateProfileName(name: String) { + controllerScope.launch { + combinedProfile.value.data?.selectedProfile?.let { + updateProfileUseCase( + modifier = UpdateProfileUseCase.Companion.ProfileModifier.Name(name), + id = it.id + ) + } + } + } + + fun deleteProfile(profileId: ProfileIdentifier, profileName: String) { + controllerScope.launch { + deleteProfileUseCase(profileId, profileName) + } + } + + fun addNewProfile(name: String) { + controllerScope.launch { + addProfileUseCase(name) + } + } +} + +@Composable +fun rememberProfileScreenController(profileId: ProfileIdentifier): ProfileScreenController { + val getProfileByIdUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val updateProfileUseCase by rememberInstance() + val switchActiveProfileUseCase by rememberInstance() + val addProfileUseCase by rememberInstance() + val deleteProfileUseCase by rememberInstance() + val logoutProfileUseCase by rememberInstance() + + return remember(profileId) { + ProfileScreenController( + profileId = profileId, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + updateProfileUseCase = updateProfileUseCase, + addProfileUseCase = addProfileUseCase, + deleteProfileUseCase = deleteProfileUseCase, + logoutProfileUseCase = logoutProfileUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt deleted file mode 100644 index 59882a38..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import android.graphics.BitmapFactory -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.AddAPhoto -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults - -@Composable -fun Avatar( - modifier: Modifier, - emptyIcon: ImageVector, - profile: ProfilesUseCaseData.Profile, - ssoStatusColor: Color?, - active: Boolean = false, - iconModifier: Modifier -) { - val currentSelectedColors = profileColor(profileColorNames = profile.color) - - Box( - modifier = modifier - .fillMaxSize() - .aspectRatio(1f), - contentAlignment = Alignment.Center - ) { - Surface( - modifier = Modifier - .fillMaxSize(), - shape = CircleShape, - color = currentSelectedColors.backGroundColor, - border = if (active) BorderStroke(2.dp, currentSelectedColors.borderColor) else null - ) { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ChooseAvatar( - profile = profile, - emptyIcon = emptyIcon, - modifier = iconModifier, - avatar = profile.avatar - ) - } - } - if (ssoStatusColor != null) { - CircleBox( - backgroundColor = ssoStatusColor, - border = BorderStroke(2.dp, MaterialTheme.colors.background), - modifier = Modifier - .size(PaddingDefaults.Medium) - .align(Alignment.BottomEnd) - .offset(PaddingDefaults.Tiny, PaddingDefaults.Tiny) - ) - } - } -} - -@Composable -private fun CircleBox( - backgroundColor: Color, - modifier: Modifier, - border: BorderStroke? = null -) { - Box( - modifier = modifier - .clip(CircleShape) - .aspectRatio(1f) - .background(backgroundColor) - .then( - border?.let { Modifier.border(border, CircleShape) } ?: Modifier - ) - ) -} - -@Preview -@Composable -private fun AvatarPreview() { - AppTheme { - Avatar( - modifier = Modifier.size(36.dp), - profile = ProfilesUseCaseData.Profile( - id = "", - name = "", - insurance = de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation( - insuranceType = ProfilesUseCaseData.InsuranceType.NONE - ), - active = false, - color = ProfilesData.ProfileColorNames.SUN_DEW, - avatar = ProfilesData.Avatar.PersonalizedImage, - image = null, - lastAuthenticated = null, - ssoTokenScope = null - ), - ssoStatusColor = null, - active = false, - iconModifier = Modifier.size(20.dp), - emptyIcon = Icons.Rounded.AddAPhoto - ) - } -} - -@Preview -@Composable -private fun AvatarWithSSOPreview() { - AppTheme { - Avatar( - modifier = Modifier.size(36.dp), - profile = ProfilesUseCaseData.Profile( - id = "", - name = "", - insurance = de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation( - insuranceType = ProfilesUseCaseData.InsuranceType.NONE - ), - active = false, - - color = ProfilesData.ProfileColorNames.SUN_DEW, - avatar = ProfilesData.Avatar.PersonalizedImage, - image = null, - lastAuthenticated = null, - ssoTokenScope = null - ), - ssoStatusColor = Color.Green, - active = false, - iconModifier = Modifier.size(20.dp), - emptyIcon = Icons.Rounded.AddAPhoto - ) - } -} - -@Composable -fun ChooseAvatar( - modifier: Modifier = Modifier, - useSmallImages: Boolean? = false, - profile: ProfilesUseCaseData.Profile, - emptyIcon: ImageVector, - showPersonalizedImage: Boolean = true, - avatar: ProfilesData.Avatar -) { - val imageResource = extractImageResource(useSmallImages, avatar) - - when (avatar) { - ProfilesData.Avatar.PersonalizedImage -> { - if (showPersonalizedImage) { - if (profile.image != null) { - BitmapImage(profile) - } else { - Icon( - emptyIcon, - modifier = modifier, - contentDescription = null, - tint = AppTheme.colors.neutral600 - ) - } - } - } - - else -> { - if (imageResource == 0) { - Icon( - emptyIcon, - modifier = modifier, - contentDescription = null, - tint = AppTheme.colors.neutral600 - ) - } else { - Image( - painterResource(id = imageResource), - null, - modifier = Modifier.fillMaxSize() - ) - } - } - } -} - -@Suppress("ComplexMethod") -@Composable -private fun extractImageResource( - useSmallImages: Boolean? = false, - figure: ProfilesData.Avatar -) = if (useSmallImages == true) { - when (figure) { - ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_small_portrait - ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_small_portrait - ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_small_portrait - ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_small_portrait - ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_small_portrait - ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_small_portrait - ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_small_portrait - ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_small_portrait - ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_small_portrait - ProfilesData.Avatar.Baby -> R.drawable.baby_small_portrait - ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_small_portrait - ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_small_portrait - ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_small_portrait - else -> 0 - } -} else { - when (figure) { - ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_portrait - ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_portrait - ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_portrait - ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_portrait - ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_portrait - ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_portrait - ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_portrait - ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_portrait - ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_portrait - ProfilesData.Avatar.Baby -> R.drawable.baby_portrait - ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_portrait - ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_portrait - ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_portrait - else -> 0 - } -} - -@Composable -private fun BitmapImage(profile: ProfilesUseCaseData.Profile) { - val bitmap by produceState(initialValue = null, profile) { - value = profile.image?.let { - BitmapFactory.decodeByteArray(profile.image, 0, it.size).asImageBitmap() - } - } - - bitmap?.let { - Image( - bitmap = it, - contentDescription = null, - modifier = Modifier.fillMaxSize() - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileAuditEventsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileAuditEventsScreen.kt deleted file mode 100644 index 11eda506..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileAuditEventsScreen.kt +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController -import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.prescription.presentation.rememberRefreshPrescriptionsController -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.rememberAuditEventsController -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.protocol.model.AuditEventData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ConnectBottomBar -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.phrasedDateString -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime - -@Requirement( - "O.Auth_5#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Screen displaying the audit events." -) -class ProfileAuditEventsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } - val profilesController = rememberProfileController() - val profiles by profilesController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val tokenValid = selectedProfile.isSSOTokenValid() - val auditEventsController = rememberAuditEventsController() - val auditItems = auditEventsController.auditEventPagingFlow.collectAsLazyPagingItems() - - val listState = rememberLazyListState() - val mainScreenController = rememberMainScreenController() - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) - - LaunchedEffect(Unit) { - auditEventsController.refresh(profileId, false, {}, {}) - } - - AnimatedElevationScaffold( - modifier = Modifier.testTag(TestTag.Profile.AuditEvents.AuditEventsScreen), - listState = listState, - topBarTitle = stringResource(R.string.autitEvents_headline), - onBack = { navController.popBackStack() }, - bottomBar = { - if (!tokenValid) { - ConnectBottomBar( - infoText = stringResource(R.string.audit_events_connect_info) - ) { - refreshPrescriptionsController.refresh( - profileId = profileId, - isUserAction = true, - onUserNotAuthenticated = {}, - onShowCardWall = { - profilesController.switchActiveProfile(selectedProfile.id) - navController.navigate( - CardWallRoutes.CardWallIntroScreen.path(selectedProfile.id) - ) - } - ) - } - } - }, - navigationMode = NavigationBarMode.Back - ) { innerPadding -> - - RefreshScaffold( - profileId = profileId, - onUserNotAuthenticated = {}, - mainScreenController = mainScreenController, - onShowCardWall = { - profilesController.switchActiveProfile(selectedProfile.id) - navController.navigate( - CardWallRoutes.CardWallIntroScreen.path(selectedProfile.id) - ) - } - ) { - if (auditItems.itemCount == 0) { - AuditEventsEmptyScreenContent( - innerPadding, - listState, - tokenValid - ) - } else { - AuditEventsScreenContent( - innerPadding, - listState, - auditItems - ) - } - } - } - } - } -} - -@Composable -private fun AuditEventsEmptyScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - tokenValid: Boolean -) { - LazyColumn( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - state = listState - ) { - item { - Column( - modifier = Modifier.padding(PaddingDefaults.Medium).fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(R.string.no_audit_events_header), - modifier = Modifier.testTag(TestTag.Profile.AuditEvents.NoAuditEventHeader), - style = AppTheme.typography.subtitle1 - ) - if (tokenValid) { - SpacerSmall() - Text( - stringResource(R.string.no_audit_events_empty_protocol_list_info), - style = AppTheme.typography.body2l, - modifier = Modifier.testTag(TestTag.Profile.AuditEvents.NoAuditEventInfo), - textAlign = TextAlign.Center - ) - } - } - } - } -} - -@Composable -private fun AuditEventsScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - auditItems: LazyPagingItems -) { - LazyColumn( - modifier = Modifier.padding(innerPadding), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) - .asPaddingValues() - ) { - items( - count = auditItems.itemCount, - key = auditItems.itemKey { it.auditId } - ) { index -> - val auditEvent = auditItems[index] - auditEvent?.let { - Column( - modifier = Modifier.padding(PaddingDefaults.Medium) - .testTag(TestTag.Profile.AuditEvents.AuditEvent) - ) { - Text(auditEvent.description, style = AppTheme.typography.body2) - - val timestamp = remember { - auditEvent.timestamp - .toLocalDateTime(TimeZone.currentSystemDefault()) - .toJavaLocalDateTime() - } - Text( - phrasedDateString(date = timestamp), - style = AppTheme.typography.body2l - ) - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColor.kt deleted file mode 100644 index 64e316c0..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColor.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.theme.AppTheme - -@Immutable -data class ProfileColor(val textColor: Color, val colorName: String, val backGroundColor: Color, val borderColor: Color) - -@Composable -fun profileColor(profileColorNames: ProfilesData.ProfileColorNames): ProfileColor { - return when (profileColorNames) { - ProfilesData.ProfileColorNames.SPRING_GRAY -> ProfileColor( - textColor = AppTheme.colors.neutral700, - colorName = stringResource(R.string.profile_color_name_gray), - backGroundColor = AppTheme.colors.neutral200, - borderColor = AppTheme.colors.neutral400 - ) - ProfilesData.ProfileColorNames.SUN_DEW -> ProfileColor( - textColor = AppTheme.colors.yellow700, - colorName = stringResource(R.string.profile_color_sun_dew), - backGroundColor = AppTheme.colors.yellow200, - borderColor = AppTheme.colors.yellow400 - ) - ProfilesData.ProfileColorNames.PINK -> ProfileColor( - textColor = AppTheme.colors.red700, - colorName = stringResource(R.string.profile_color_name_pink), - backGroundColor = AppTheme.colors.red200, - borderColor = AppTheme.colors.red400 - ) - ProfilesData.ProfileColorNames.TREE -> ProfileColor( - textColor = AppTheme.colors.green700, - colorName = stringResource(R.string.profile_color_name_tree), - backGroundColor = AppTheme.colors.green200, - borderColor = AppTheme.colors.green400 - ) - ProfilesData.ProfileColorNames.BLUE_MOON -> ProfileColor( - textColor = AppTheme.colors.primary700, - colorName = stringResource(R.string.profile_color_name_moon), - backGroundColor = AppTheme.colors.primary200, - borderColor = AppTheme.colors.primary400 - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditPictureScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditPictureScreen.kt deleted file mode 100644 index 9ac96c0a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditPictureScreen.kt +++ /dev/null @@ -1,396 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Done -import androidx.compose.material.icons.rounded.AddAPhoto -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.PersonOutline -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource - -class ProfileEditPictureScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } - val profilesController = rememberProfileController() - val profiles by profilesController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val listState = rememberLazyListState() - var editableProfile by remember { mutableStateOf(selectedProfile) } - - Scaffold( - modifier = Modifier.imePadding(), - topBar = { - NavigationTopAppBar( - navigationMode = NavigationBarMode.Back, - title = stringResource(R.string.edit_profile_picture), - onBack = { navController.popBackStack() }, - actions = {} - ) - } - ) { paddingValues -> - LazyColumn( - state = listState, - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - contentPadding = PaddingValues(PaddingDefaults.Medium) - ) { - item { - SpacerMedium() - ProfileImage(editableProfile) { - editableProfile = editableProfile.copy( - avatar = ProfilesData.Avatar.PersonalizedImage, - image = null - ) - profilesController.clearPersonalizedImage(selectedProfile.id) - } - } - item { - SpacerXXLarge() - AvatarPicker( - profile = editableProfile, - currentAvatar = editableProfile.avatar, - onPickPersonalizedImage = { - navController.navigate( - ProfileRoutes.ProfileImageCropperScreen.path(profileId = selectedProfile.id) - ) - }, - onSelectAvatar = { - editableProfile = editableProfile.copy(avatar = it) - profilesController.saveAvatarFigure(selectedProfile.id, it) - } - ) - } - - if (editableProfile.avatar != ProfilesData.Avatar.PersonalizedImage) { - item { - SpacerXXLarge() - SpacerMedium() - Text( - stringResource(R.string.edit_profile_background_color), - style = AppTheme.typography.h6 - ) - SpacerLarge() - ColorPicker( - profileColorName = editableProfile.color, - onSelectProfileColor = { - editableProfile = editableProfile.copy(color = it) - profilesController.updateProfileColor(selectedProfile, it) - } - ) - } - } - } - } - } - } -} - -@Composable -fun AvatarPicker( - profile: ProfilesUseCaseData.Profile, - currentAvatar: ProfilesData.Avatar?, - onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.Avatar) -> Unit -) { - val listState = rememberLazyListState() - - LazyRow( - state = listState, - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) - ) { - ProfilesData.Avatar.entries.forEach { figure -> - item { - AvatarSelector( - figure = figure, - profile = profile, - selected = figure == currentAvatar, - onPickPersonalizedImage = onPickPersonalizedImage, - onSelectAvatar = onSelectAvatar - ) - } - } - } -} - -@Composable -private fun AvatarSelector( - profile: ProfilesUseCaseData.Profile, - figure: ProfilesData.Avatar, - selected: Boolean, - onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.Avatar) -> Unit -) { - Surface( - modifier = Modifier - .size(80.dp), - shape = CircleShape, - border = if (selected) { - BorderStroke(5.dp, color = AppTheme.colors.primary600) - } else if (figure != ProfilesData.Avatar.PersonalizedImage) { - BorderStroke(1.dp, color = AppTheme.colors.neutral300) - } else { - null - } - ) { - Row( - modifier = Modifier - .background( - color = when (figure) { - ProfilesData.Avatar.PersonalizedImage -> { - AppTheme.colors.neutral100 - } - - else -> { - AppTheme.colors.neutral025 - } - } - ) - .clickable(onClick = { - if (figure == ProfilesData.Avatar.PersonalizedImage) { - onPickPersonalizedImage() - onSelectAvatar(figure) - } - onSelectAvatar(figure) - }), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - ChooseAvatar( - useSmallImages = true, - emptyIcon = Icons.Rounded.AddAPhoto, - modifier = Modifier.size(24.dp), - profile = profile, - avatar = figure - ) - } - } -} - -@Composable -fun ColorPicker( - profileColorName: ProfilesData.ProfileColorNames, - onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit -) { - val currentSelectedColors = profileColor(profileColorNames = profileColorName) - - Column(modifier = Modifier.fillMaxWidth()) { - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - ProfilesData.ProfileColorNames.entries.forEach { - val currentValueColors = profileColor(profileColorNames = it) - ColorSelector( - modifier = Modifier.testTag( - when (it) { - ProfilesData.ProfileColorNames.SPRING_GRAY -> - TestTag.Profile.EditProfileIcon.ColorSelectorSpringGrayButton - - ProfilesData.ProfileColorNames.SUN_DEW -> - TestTag.Profile.EditProfileIcon.ColorSelectorSunDewButton - - ProfilesData.ProfileColorNames.PINK -> - TestTag.Profile.EditProfileIcon.ColorSelectorPinkButton - - ProfilesData.ProfileColorNames.TREE -> - TestTag.Profile.EditProfileIcon.ColorSelectorTreeButton - - ProfilesData.ProfileColorNames.BLUE_MOON -> - TestTag.Profile.EditProfileIcon.ColorSelectorBlueMoonButton - } - ), - profileColorName = it, - selected = currentValueColors == currentSelectedColors, - onSelectColor = onSelectProfileColor - ) - } - } - SpacerMedium() - Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { - Text( - currentSelectedColors.colorName, - style = AppTheme.typography.body2l - ) - } - } -} - -@Composable -private fun createProfileColor(colors: ProfilesData.ProfileColorNames): ProfileColor { - return profileColor(profileColorNames = colors) -} - -@Composable -private fun ColorSelector( - modifier: Modifier, - profileColorName: ProfilesData.ProfileColorNames, - selected: Boolean, - onSelectColor: (ProfilesData.ProfileColorNames) -> Unit -) { - val colors = createProfileColor(profileColorName) - val contentDescription = annotatedStringResource( - R.string.edit_profile_color_selected, - profileColorName.name - ).toString() - - Surface( - modifier = modifier - .size(40.dp) - .clip(CircleShape) - .clickable(onClick = { onSelectColor(profileColorName) }), - color = colors.backGroundColor - ) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - if (selected) { - Icon( - Icons.Outlined.Done, - contentDescription, - tint = colors.textColor, - modifier = Modifier.size(24.dp) - ) - } - } - } -} - -@Composable -fun ProfileImage( - selectedProfile: ProfilesUseCaseData.Profile, - onClickDeleteAvatar: () -> Unit -) { - val colors = profileColor(profileColorNames = selectedProfile.color) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - .semantics(true) { - stateDescription = selectedProfile.color.name - } - ) { - Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { - Box( - modifier = Modifier - .size(160.dp) - .clip(CircleShape) - .aspectRatio(1f) - .background(colors.backGroundColor), - contentAlignment = Alignment.Center - ) { - ChooseAvatar( - modifier = Modifier.size(36.dp), - profile = selectedProfile, - emptyIcon = Icons.Rounded.PersonOutline, - avatar = selectedProfile.avatar, - showPersonalizedImage = selectedProfile.image != null - ) - } - if (!(selectedProfile.hasNoImageSelected())) { - @Suppress("MagicNumber") - Box( - modifier = Modifier - .size(32.dp) - .align(Alignment.TopEnd) - .offset((-8).dp, 4.dp) - .clip(CircleShape) - .aspectRatio(1f) - .background(AppTheme.colors.neutral050) - .border(1.dp, color = AppTheme.colors.neutral000, shape = RoundedCornerShape(16.dp)) - ) { - IconButton( - onClick = { - onClickDeleteAvatar() - } - ) { - Icon( - imageVector = Icons.Rounded.Close, - tint = AppTheme.colors.neutral600, - contentDescription = null - ) - } - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreen.kt deleted file mode 100644 index 9b00b41c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreen.kt +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.foundation.MutatorMutex -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator -import de.gematik.ti.erp.app.cardwall.mini.ui.NoneEnrolledException -import de.gematik.ti.erp.app.cardwall.mini.ui.RedirectUrlWrongException -import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException -import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.ProfileController -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.annotatedStringBold -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import de.gematik.ti.erp.app.utils.compose.toAnnotatedString -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.retry -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.io.IOException -import java.net.UnknownHostException -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -private const val VERTICAL_BIAS_ALIGNMENT = -0.33f -private const val HORIZONTAL_BIAS_ALIGNMENT = 0f - -class ProfilePairedDevicesScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } - val profilesController = rememberProfileController() - val profiles by profilesController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.paired_devices_title), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = { navController.popBackStack() } - ) { - PairedDevices( - modifier = Modifier.padding(it), - selectedProfile = selectedProfile, - profileController = profilesController, - listState = listState - ) - } - } - } -} - -@Stable -private sealed interface RefreshState { - @Stable - object Loading : RefreshState - - @Stable - class WithResults(val result: ProfilesUseCaseData.PairedDevices) : RefreshState - - @Stable - object NoResults : RefreshState - - @Stable - class Error(val throwable: Throwable) : RefreshState -} - -@Stable -private sealed interface DeleteState { - @Stable - class Deleting(val device: PairedDevice, val isThisDevice: Boolean) : DeleteState - - @Stable - object None : DeleteState - - @Stable - object Error : DeleteState -} - -// tag::PairedDevicesUI[] -@Suppress("ComplexMethod") -@Composable -private fun PairedDevices( - modifier: Modifier, - selectedProfile: ProfilesUseCaseData.Profile, - profileController: ProfileController, - listState: LazyListState -) { - val authenticator = LocalAuthenticator.current - - val refreshFlow = remember { MutableSharedFlow() } - var state by remember { mutableStateOf(RefreshState.NoResults) } - LaunchedEffect(selectedProfile) { - refreshFlow - .onStart { emit(Unit) } // emit once to start the flow directly - .collectLatest { - state = RefreshState.Loading - profileController - .pairedDevices(selectedProfile.id) - .retry(1) { throwable -> - Napier.e("Couldn't get paired devices", throwable) - if (throwable is RefreshFlowException && throwable.isUserAction) { - authenticator - .authenticateForPairedDevices(selectedProfile.id) - .first() - .let { - when (it) { - PromptAuthenticator.AuthResult.Authenticated -> true - PromptAuthenticator.AuthResult.Cancelled -> false - PromptAuthenticator.AuthResult.NoneEnrolled -> - throw NoneEnrolledException() - PromptAuthenticator.AuthResult.UserNotAuthenticated -> - throw UserNotAuthenticatedException() - PromptAuthenticator.AuthResult.RedirectLinkNotRight -> - throw RedirectUrlWrongException() - } - } - } else { - false - } - } - .catch { - Napier.d("Couldn't get paired devices", it) - - state = RefreshState.Error(it) - } - .collect { - state = if (it.devices.isEmpty()) { - RefreshState.NoResults - } else { - RefreshState.WithResults(it) - } - } - } - } - - val keyStoreAlias = remember(selectedProfile) { - (selectedProfile.ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope) - ?.aliasOfSecureElementEntryBase64() - } - - val mutex = MutatorMutex() - val coroutineScope = rememberCoroutineScope() - - var deleteState by remember(state) { mutableStateOf(null) } - - (deleteState as? DeleteState.Deleting)?.let { - DeleteDeviceDialog( - device = it.device, - isThisDevice = it.isThisDevice, - onCancel = { - deleteState = DeleteState.None - }, - onClickAction = { - coroutineScope.launch { - mutex.mutate { - profileController - .deletePairedDevice(selectedProfile.id, it.device) - .onFailure { - deleteState = DeleteState.Error - } - .onSuccess { - deleteState = DeleteState.None - } - - // no matter if we received an error or not, we need to refresh this list - refreshFlow.emit(Unit) - } - } - } - ) - } - - LazyColumn( - state = listState, - modifier = modifier - ) { - when (state) { - RefreshState.Loading -> item { EmptyScreenLoading(Modifier.fillParentMaxSize()) } - RefreshState.NoResults -> item { EmptyScreenNoDevices(Modifier.fillParentMaxSize()) } - is RefreshState.Error -> item { - val (title, desc) = errorMessageFromException((state as RefreshState.Error).throwable) - EmptyScreenFailure( - modifier = Modifier.fillParentMaxSize(), - title = title, - description = desc, - onClickRetry = { - coroutineScope.launch { - refreshFlow.emit(Unit) - } - } - ) - } - - is RefreshState.WithResults -> { - val devices = (state as RefreshState.WithResults).result.devices - items(items = devices) { device -> - val isThisDevice = keyStoreAlias?.let { - device.isOurDevice(it) - } ?: false - PairedDevice( - device = device, - isOurDevice = isThisDevice, - onDeleteDevice = { - deleteState = DeleteState.Deleting(device, isThisDevice) - } - ) - } - } - } - } -} -// end::PairedDevicesUI[] - -@Composable -private fun DeleteDeviceDialog( - device: PairedDevice, - isThisDevice: Boolean, - onCancel: () -> Unit, - onClickAction: () -> Unit -) { - if (isThisDevice) { - DeleteThisDeviceDialog( - device = device, - onCancel = onCancel, - onClickAction = onClickAction - ) - } else { - DeleteOtherDeviceDialog( - device = device, - onCancel = onCancel, - onClickAction = onClickAction - ) - } -} - -@Composable -private fun DeleteOtherDeviceDialog( - device: PairedDevice, - onCancel: () -> Unit, - onClickAction: () -> Unit -) { - CommonAlertDialog( - header = stringResource(R.string.paired_devices_delete_title).toAnnotatedString(), - info = annotatedStringResource(R.string.paired_devices_delete_description, annotatedStringBold(device.name)), - cancelText = stringResource(R.string.paired_devices_delete_cancel), - actionText = stringResource(R.string.paired_devices_delete_remove), - onCancel = onCancel, - onClickAction = onClickAction - ) -} - -@Composable -private fun DeleteThisDeviceDialog( - device: PairedDevice, - onCancel: () -> Unit, - onClickAction: () -> Unit -) { - CommonAlertDialog( - header = stringResource(R.string.paired_devices_delete_this_title).toAnnotatedString(), - info = annotatedStringResource( - R.string.paired_devices_delete_this_description, - annotatedStringBold(device.name) - ), - cancelText = stringResource(R.string.paired_devices_delete_cancel), - actionText = stringResource(R.string.paired_devices_delete_remove), - onCancel = onCancel, - onClickAction = onClickAction - ) -} - -@Composable -private fun EmptyScreenLoading(modifier: Modifier) { - EmptyScreen(modifier) { - CircularProgressIndicator(Modifier.size(48.dp)) - Text( - stringResource(R.string.paired_devices_loading_description), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun EmptyScreenNoDevices(modifier: Modifier) { - EmptyScreen(modifier) { - Text( - stringResource(R.string.paired_devices_no_devices_title), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.paired_devices_no_devices_description), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } -} - -@Composable -private fun EmptyScreenFailure( - modifier: Modifier, - title: String, - description: String, - onClickRetry: () -> Unit -) { - EmptyScreen(modifier) { - Text( - title, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - Text( - description, - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - TextButton(onClick = onClickRetry) { - Icon(Icons.Rounded.Refresh, null) - SpacerSmall() - Text(stringResource(R.string.paired_devices_error_retry)) - } - } -} - -@Suppress("MagicNumber") -@Composable -private fun EmptyScreen( - modifier: Modifier, - content: @Composable () -> Unit -) { - Box(modifier) { - Column( - modifier = Modifier - .align(BiasAlignment(HORIZONTAL_BIAS_ALIGNMENT, VERTICAL_BIAS_ALIGNMENT)) - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - content() - } - } -} - -@Composable -private fun PairedDevice( - device: PairedDevice, - isOurDevice: Boolean, - onDeleteDevice: () -> Unit -) { - Row(Modifier.padding(PaddingDefaults.Medium)) { - Column(Modifier.weight(1f)) { - Text(device.name, style = AppTheme.typography.body1) - val connectedOn = localizedDateString(device.connectedOn) - if (isOurDevice) { - Text( - stringResource(R.string.paired_device_subtitle_our_device, connectedOn), - style = AppTheme.typography.body2l - ) - } else { - Text(stringResource(R.string.paired_device_subtitle, connectedOn), style = AppTheme.typography.body2l) - } - } - SpacerMedium() - IconButton(onClick = onDeleteDevice) { - Icon(Icons.Rounded.Delete, null, tint = AppTheme.colors.neutral500) - } - } -} - -@Composable -private fun localizedDateString(timestamp: Instant, format: FormatStyle = FormatStyle.LONG): String { - val config = LocalConfiguration.current - return remember(config, format) { - val fmt = DateTimeFormatter.ofLocalizedDate(format) - timestamp.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime().format(fmt) - } -} - -@Composable -private fun errorMessageFromException(t: Throwable): Pair { - val other = stringResource(R.string.paired_devices_error_generic_title) to - stringResource(R.string.paired_devices_error_generic_description) - val network = stringResource(R.string.paired_devices_error_no_network_title) to - stringResource(R.string.paired_devices_error_no_network_description) - - return when (t) { - is IOException -> when { - t.cause is UnknownHostException -> network - else -> other - } - - else -> other - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreen.kt deleted file mode 100644 index 0563f548..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreen.kt +++ /dev/null @@ -1,826 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.appendInlineContent -import androidx.compose.material.Divider -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CloudQueue -import androidx.compose.material.icons.outlined.Edit -import androidx.compose.material.icons.outlined.VpnKey -import androidx.compose.material.icons.rounded.AddAPhoto -import androidx.compose.material.icons.rounded.Devices -import androidx.compose.material.icons.rounded.EuroSymbol -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.Placeholder -import androidx.compose.ui.text.PlaceholderVerticalAlign -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.TestTag.Profile.OpenTokensScreenButton -import de.gematik.ti.erp.app.TestTag.Profile.ProfileScreen -import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.ProfileController -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.settings.ui.ProfileNameDialog -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.DynamicText -import de.gematik.ti.erp.app.utils.compose.ErrorText -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.visualTestTag -import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant - -class ProfileScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Suppress("LongMethod") - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } - val profileController = rememberProfileController() - val profiles by profileController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val listState = rememberLazyListState() - val scaffoldState = rememberScaffoldState() - var showAddDefaultProfileDialog by remember { mutableStateOf(false) } - var deleteProfileDialogVisible by remember { mutableStateOf(false) } - - if (deleteProfileDialogVisible) { - DeleteProfileDialog( - onCancel = { deleteProfileDialogVisible = false }, - onClickAction = { - if (profiles.size == 1) { - showAddDefaultProfileDialog = true - } else { - profileController.removeProfile(selectedProfile, null) - navController.popBackStack() - } - deleteProfileDialogVisible = false - } - ) - } - AnimatedElevationScaffold( - modifier = Modifier - .imePadding() - .visualTestTag(ProfileScreen), - topBarTitle = stringResource(R.string.edit_profile_title), - navigationMode = NavigationBarMode.Back, - scaffoldState = scaffoldState, - listState = listState, - actions = { - ThreeDotMenu( - selectedProfile = selectedProfile, - onClickLogIn = { - profileController.switchActiveProfile(selectedProfile.id) - navController.navigate( - CardWallRoutes.CardWallIntroScreen.path(selectedProfile.id) - ) - }, - onClickLogout = { profileController.logout(selectedProfile) }, - onClickDelete = { deleteProfileDialogVisible = true } - ) - }, - onBack = { navController.popBackStack() } - ) { - if (showAddDefaultProfileDialog) { - ProfileNameDialog( - profileController = profileController, - wantRemoveLastProfile = true, - onEdit = { - showAddDefaultProfileDialog = false - profileController.removeProfile(selectedProfile, it) - navController.popBackStack() - }, - onDismissRequest = { showAddDefaultProfileDialog = false } - ) - } - ProfileScreenContent( - listState, - profileController, - profiles, - selectedProfile, - navController - ) - } - } - } -} - -@Composable -private fun ProfileScreenContent( - listState: LazyListState, - profileController: ProfileController, - profiles: List, - selectedProfile: ProfilesUseCaseData.Profile, - navController: NavController -) { - LazyColumn( - modifier = Modifier.testTag(TestTag.Profile.ProfileScreenContent), - state = listState, - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - ProfileNameSection( - profile = selectedProfile, - profiles = profiles, - onChangeProfileName = { - profileController.updateProfileName(selectedProfile.id, it) - } - ) - } - item { - SpacerLarge() - ProfileAvatarSection( - profile = selectedProfile, - onClickEditAvatar = { - navController.navigate( - ProfileRoutes.ProfileEditPictureScreen.path(profileId = selectedProfile.id) - ) - } - ) - } - item { - ProfileInsuranceInformation( - selectedProfile.lastAuthenticated, - selectedProfile.ssoTokenScope, - selectedProfile.insurance, - onClickLogIn = { - profileController.switchActiveProfile(selectedProfile.id) - navController.navigate( - CardWallRoutes.CardWallIntroScreen.path(selectedProfile.id) - ) - } - ) - } - - if (selectedProfile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { - item { - ProfileInvoiceInformation { - navController.navigate( - PkvRoutes.InvoiceListScreen.path(profileId = selectedProfile.id) - ) - } - } - } - - if (selectedProfile.ssoTokenScope != null && selectedProfile.isBiometricPairing()) { - item { - ProfileEditPairedDeviceSection( - onShowPairedDevices = { - navController.navigate( - ProfileRoutes.ProfilePairedDevicesScreen.path(profileId = selectedProfile.id) - ) - } - ) - } - } - @Requirement( - "O.Tokn_5#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Section for Access and SSO Token display" - ) - item { - SecuritySection( - onClickAuditEvents = { - navController.navigate( - ProfileRoutes.ProfileAuditEventsScreen.path(profileId = selectedProfile.id) - ) - }, - onClickToken = { - navController.navigate( - ProfileRoutes.ProfileTokenScreen.path(profileId = selectedProfile.id) - ) - } - ) - } - } -} - -@Composable -private fun ProfileEditPairedDeviceSection( - onShowPairedDevices: () -> Unit -) { - Text( - text = stringResource(R.string.settings_paired_devices_title), - style = AppTheme.typography.h6, - modifier = Modifier.padding(PaddingDefaults.Medium) - ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = onShowPairedDevices - ) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(Icons.Rounded.Devices, null, tint = AppTheme.colors.primary600) - Text(stringResource(R.string.settings_login_connected_devices), style = AppTheme.typography.body1) - } - SpacerLarge() - Divider(modifier = Modifier.padding(horizontal = PaddingDefaults.Medium)) - SpacerLarge() -} - -@Composable -private fun ProfileInvoiceInformation(onClick: () -> Unit) { - Column { - Text( - stringResource( - id = R.string.profile_invoiceInformation_header - ), - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - style = AppTheme.typography.h6 - ) - SpacerSmall() - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = onClick - ) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(Icons.Rounded.EuroSymbol, null, tint = AppTheme.colors.primary600) - Text( - stringResource( - R.string.profile_show_invoices - ), - style = AppTheme.typography.body1 - ) - } - SpacerLarge() - Divider() - SpacerLarge() - } -} - -@Composable -private fun ThreeDotMenu( - selectedProfile: ProfilesUseCaseData.Profile, - onClickLogIn: () -> Unit, - onClickLogout: () -> Unit, - onClickDelete: () -> Unit -) { - var expanded by remember { mutableStateOf(false) } - - IconButton( - onClick = { expanded = true }, - modifier = Modifier.testTag(TestTag.Profile.ThreeDotMenuButton) - ) { - Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - offset = DpOffset(24.dp, 0.dp) - ) { - @Requirement( - "O.Tokn_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "A logout button is available within each user profile." - ) - DropdownMenuItem( - modifier = Modifier.testTag( - if (selectedProfile.ssoTokenScope != null) { - TestTag.Profile.LogoutButton - } else { - TestTag.Profile.LoginButton - } - ), - onClick = if (selectedProfile.ssoTokenScope != null) { - onClickLogout - } else { - onClickLogIn - } - ) { - Text( - text = if (selectedProfile.ssoTokenScope != null) { - stringResource(R.string.insurance_information_logout) - } else { - stringResource(R.string.insurance_information_login) - } - ) - } - - DropdownMenuItem( - modifier = Modifier.testTag(TestTag.Profile.DeleteProfileButton), - onClick = { - expanded = false - onClickDelete() - } - ) { - Text( - text = stringResource(R.string.remove_profile), - color = AppTheme.colors.red600 - ) - } - } -} - -@Composable -private fun DeleteProfileDialog(onCancel: () -> Unit, onClickAction: () -> Unit) { - CommonAlertDialog( - header = stringResource(id = R.string.remove_profile_header), - info = stringResource(R.string.remove_profile_detail_message), - actionText = stringResource(R.string.remove_profile_yes), - cancelText = stringResource(R.string.remove_profile_no), - onCancel = onCancel, - onClickAction = onClickAction - ) -} - -@Composable -private fun SecuritySection( - onClickToken: () -> Unit, - onClickAuditEvents: () -> Unit -) { - Text( - text = stringResource(R.string.settings_appprotection_headline), - style = AppTheme.typography.h6, - modifier = Modifier.padding(PaddingDefaults.Medium) - ) - SecurityTokenSubSection(onClickToken) - SecurityAuditEventsSubSection(onClickAuditEvents) -} - -@Requirement( - "A_19177", - "A_19185", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Button to display audit events for profile." -) -@Requirement( - "O.Auth_5#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Button to display audit events for profile." -) -@Composable -private fun SecurityAuditEventsSubSection(onClickAuditEvents: () -> Unit) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = { - onClickAuditEvents() - } - ) - .testTag(TestTag.Profile.OpenAuditEventsScreenButton) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(Icons.Outlined.CloudQueue, null, tint = AppTheme.colors.primary600) - Column { - Text( - stringResource( - R.string.settings_show_audit_events - ), - style = AppTheme.typography.body1 - ) - Text( - stringResource( - R.string.settings_show_audit_events_info - ), - style = AppTheme.typography.body2l - ) - } - } -} - -@Composable -private fun SecurityTokenSubSection(onClick: () -> Unit) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Top, - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = { onClick() } - ) - .visualTestTag(OpenTokensScreenButton) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(Icons.Outlined.VpnKey, null, tint = AppTheme.colors.primary600) - Column { - Text( - stringResource( - R.string.settings_show_token - ), - style = AppTheme.typography.body1 - ) - Text( - stringResource( - R.string.settings_show_token_info - ), - style = AppTheme.typography.body2l - ) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun ProfileNameSection( - profile: ProfilesUseCaseData.Profile, - profiles: List, - onChangeProfileName: (String) -> Unit -) { - var profileName by remember(profile.name) { mutableStateOf(profile.name) } - var profileNameValid by remember { mutableStateOf(true) } - var textFieldEnabled by remember { mutableStateOf(false) } - - val focusRequester = remember { FocusRequester() } - val scope = rememberCoroutineScope() - - val keyboardController = LocalSoftwareKeyboardController.current - - LaunchedEffect(textFieldEnabled) { - if (textFieldEnabled) { - focusRequester.requestFocus() - keyboardController?.show() - } - } - - Column { - Row( - modifier = Modifier.padding(PaddingDefaults.Medium) - ) { - if (!textFieldEnabled) { - val txt = buildAnnotatedString { - append(profileName) - append(" ") - appendInlineContent("edit", "edit") - } - val inlineContent = mapOf( - "edit" to InlineTextContent( - Placeholder( - width = 0.em, - height = 0.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter - ) - ) { - Icon(Icons.Outlined.Edit, null, tint = AppTheme.colors.neutral400) - } - ) - DynamicText( - txt, - style = AppTheme.typography.h5, - inlineContent = inlineContent, - modifier = Modifier - .clickable { - textFieldEnabled = true - } - .testTag(TestTag.Profile.EditProfileNameButton) - ) - } else { - ProfileEditBasicTextField( - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester) - .testTag(TestTag.Profile.NewProfileNameField), - enabled = textFieldEnabled, - initialProfileName = profile.name, - onChangeProfileName = { name: String, isValid: Boolean -> - profileName = name - profileNameValid = isValid - }, - profiles = profiles, - onDone = { - if (profileNameValid) { - onChangeProfileName(profileName) - textFieldEnabled = false - scope.launch { keyboardController?.hide() } - } - } - ) - } - } - - if (!profileNameValid) { - SpacerTiny() - val errorText = if (profileName.isBlank()) { - stringResource(R.string.edit_profile_empty_profile_name) - } else { - stringResource(R.string.edit_profile_duplicated_profile_name) - } - - ErrorText( - text = errorText, - modifier = Modifier.padding(start = PaddingDefaults.Medium) - ) - } - } -} - -@Composable -private fun ProfileEditBasicTextField( - modifier: Modifier, - enabled: Boolean, - textStyle: TextStyle = AppTheme.typography.h5, - initialProfileName: String, - onChangeProfileName: (String, Boolean) -> Unit, - profiles: List, - onDone: () -> Unit -) { - var profileNameState by remember { - mutableStateOf( - TextFieldValue( - text = initialProfileName, - selection = TextRange(initialProfileName.length) - ) - ) - } - - val color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) - val mergedTextStyle = textStyle.merge(TextStyle(color = color)) - - BasicTextField( - value = profileNameState, - onValueChange = { - val name = it.text.trimStart().sanitizeProfileName() - profileNameState = TextFieldValue( - text = name, - selection = it.selection, - composition = it.composition - ) - - onChangeProfileName( - name, - name.trim().equals(initialProfileName, true) || - !profiles.containsProfileWithName(name) && name.isNotEmpty() - ) - }, - enabled = enabled, - singleLine = !enabled, - textStyle = mergedTextStyle, - modifier = modifier, - cursorBrush = SolidColor(color), - keyboardOptions = KeyboardOptions( - autoCorrect = false, - capitalization = KeyboardCapitalization.Sentences, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions(onDone = { onDone() }) - ) -} - -@Composable -private fun ProfileAvatarSection( - profile: ProfilesUseCaseData.Profile, - onClickEditAvatar: () -> Unit -) { - val currentSelectedColors = profileColor(profileColorNames = profile.color) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium) - ) { - SpacerTiny() - Surface( - modifier = Modifier - .size(140.dp) - .align(Alignment.CenterHorizontally), - shape = CircleShape, - color = currentSelectedColors.backGroundColor - ) { - Box( - modifier = Modifier - .fillMaxSize() - .clickable(onClick = onClickEditAvatar), - contentAlignment = Alignment.Center - ) { - ChooseAvatar( - emptyIcon = Icons.Rounded.AddAPhoto, - modifier = Modifier.size(24.dp), - profile = profile, - avatar = profile.avatar - ) - } - } - SpacerSmall() - TextButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .testTag(TestTag.Profile.EditProfileImageButton), - onClick = onClickEditAvatar - ) { - Text(text = stringResource(R.string.edit_profile_avatar), textAlign = TextAlign.Center) - } - } -} - -@Composable -private fun ProfileInsuranceInformation( - lastAuthenticated: Instant?, - ssoTokenScope: IdpData.SingleSignOnTokenScope?, - insuranceInformation: ProfileInsuranceInformation, - onClickLogIn: () -> Unit -) { - SpacerLarge() - val cardAccessNumber = if (ssoTokenScope is IdpData.TokenWithHealthCardScope) { - ssoTokenScope.cardAccessNumber - } else { - null - } - - Column { - Text( - stringResource( - id = R.string.insurance_information_header - ), - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - style = AppTheme.typography.h6 - ) - SpacerSmall() - - if (lastAuthenticated != null) { - LabeledText( - stringResource(R.string.insurance_information_insurant_name), - insuranceInformation.insurantName - ) - LabeledText( - stringResource(R.string.insurance_information_insurance_name), - insuranceInformation.insuranceName - ) - cardAccessNumber?.let { - LabeledText(stringResource(R.string.insurance_information_insurant_can), it) - } - LabeledText( - stringResource(R.string.insurance_information_insurance_identifier), - insuranceInformation.insuranceIdentifier, - Modifier.testTag(TestTag.Profile.InsuranceId) - ) - } - - if (ssoTokenScope != null) { - LabeledText( - stringResource(R.string.profile_insurance_information_connected_label), - when (ssoTokenScope) { - is IdpData.DefaultToken -> stringResource( - R.string.profile_insurance_information_connected_health_card - ) - - is IdpData.ExternalAuthenticationToken -> ssoTokenScope.authenticatorName - is IdpData.AlternateAuthenticationToken, - is IdpData.AlternateAuthenticationWithoutToken -> stringResource( - R.string.profile_insurance_information_connected_biometrics - ) - } - ) - } else { - ClickableLabeledTextWithIcon( - description = stringResource(R.string.profile_insurance_information_connected_label), - content = stringResource(R.string.profile_insurance_information_not_connected), - icon = Icons.Rounded.Refresh - ) { - onClickLogIn() - } - } - SpacerLarge() - Divider() - SpacerLarge() - } -} - -/** - * Shows the given content if != null labeled with a description as described in design guide for ProfileScreen. - */ -@Composable -private fun LabeledText(description: String, content: String, modifier: Modifier = Modifier) { - Column(modifier.padding(PaddingDefaults.Medium)) { - Text(content, style = AppTheme.typography.body1) - Text(description, style = AppTheme.typography.body2l) - } -} - -@Composable -private fun ClickableLabeledTextWithIcon( - description: String, - content: String, - modifier: Modifier = Modifier, - icon: ImageVector, - onClick: () -> Unit -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { - onClick() - } - .padding(PaddingDefaults.Medium), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text(content, style = AppTheme.typography.body1) - Text(description, style = AppTheme.typography.body2l) - } - Icon(icon, contentDescription = null, tint = AppTheme.colors.primary600) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileTokenScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileTokenScreen.kt deleted file mode 100644 index d1d67a22..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileTokenScreen.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import android.widget.Toast -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ContentCopy -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.TestTag.Profile.TokenList.AccessToken -import de.gematik.ti.erp.app.TestTag.Profile.TokenList.NoTokenHeader -import de.gematik.ti.erp.app.TestTag.Profile.TokenList.NoTokenInfo -import de.gematik.ti.erp.app.TestTag.Profile.TokenList.SSOToken -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.visualTestTag -@Requirement( - "O.Purp_9#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Access and SSO Token display relates to O.Tokn_5" -) -@Requirement( - "O.Tokn_5#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Access and SSO Token display" -) -class ProfileTokenScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } - val profilesController = rememberProfileController() - val profiles by profilesController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> - val accessToken by profilesController.decryptedAccessToken(selectedProfile) - val ssoToken = selectedProfile.ssoTokenScope?.token?.token - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - modifier = Modifier.visualTestTag(TestTag.Profile.TokenList.TokenScreen), - navigationMode = NavigationBarMode.Back, - topBarTitle = stringResource(id = R.string.token_headline), - onBack = { navController.popBackStack() }, - listState = listState - ) { - if (accessToken == null && ssoToken == null) { - TokenScreenEmptyContent(listState) - } else { - TokenScreenContent( - listState, - accessToken, - ssoToken - ) - } - } - } - } -} - -@Composable -private fun TokenScreenEmptyContent(listState: LazyListState) { - LazyColumn( - state = listState, - modifier = Modifier - .padding(PaddingDefaults.Medium) - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - item { - Text( - stringResource(R.string.token_screen_no_token_header), - style = AppTheme.typography.subtitle1, - modifier = Modifier.visualTestTag(NoTokenHeader) - ) - SpacerSmall() - Text( - stringResource(R.string.token_screen_no_token_info), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center, - modifier = Modifier.visualTestTag(NoTokenInfo) - ) - } - } -} - -@Composable -private fun TokenScreenContent( - listState: LazyListState, - accessToken: String?, - ssoToken: String? -) { - val accessTokenTitle = stringResource(id = R.string.access_token_title) - val singleSignOnTokenTitle = stringResource(id = R.string.single_sign_on_token_title) - LazyColumn( - state = listState, - modifier = Modifier.padding(vertical = PaddingDefaults.Medium), - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - TokenLabel( - modifier = Modifier.visualTestTag(AccessToken), - title = accessTokenTitle, - text = accessToken ?: stringResource(id = R.string.no_access_token), - tokenAvailable = accessToken != null - ) - Divider(modifier = Modifier.padding(start = PaddingDefaults.Medium)) - } - item { - TokenLabel( - modifier = Modifier.visualTestTag(SSOToken), - title = singleSignOnTokenTitle, - text = ssoToken - ?: stringResource(id = R.string.no_single_sign_on_token), - tokenAvailable = ssoToken != null - ) - } - } -} - -@Composable -private fun TokenLabel(modifier: Modifier, title: String, text: String, tokenAvailable: Boolean) { - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - val copied = stringResource(R.string.copied) - val description = if (tokenAvailable) { - stringResource(R.string.copy_content_description) - } else { - stringResource(R.string.copied) - } - - val mod = if (tokenAvailable) { - modifier - .clickable(onClick = { - if (tokenAvailable) { - clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(text)) - Toast - .makeText(context, "$title $copied", Toast.LENGTH_SHORT) - .show() - } - }) - .semantics { contentDescription = description } - } else { - modifier - } - - Row( - modifier = mod - ) { - Row( - modifier = Modifier - .sizeIn(maxHeight = 200.dp) - ) { - Column( - modifier = Modifier - .padding(PaddingDefaults.Medium) - .weight(1f) - ) { - Text(title, style = AppTheme.typography.subtitle1) - LazyColumn { - item { - Text(text, style = AppTheme.typography.body2) - } - } - } - - if (tokenAvailable) { - Column( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(end = PaddingDefaults.Medium) - ) { - Icon(Icons.Outlined.ContentCopy, null, tint = AppTheme.colors.neutral400) - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/AuditEventsLoading.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/AuditEventsLoading.kt new file mode 100644 index 00000000..de41defe --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/AuditEventsLoading.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.valentinilk.shimmer.shimmer +import de.gematik.ti.erp.app.shimmer.LimitedTextShimmer +import de.gematik.ti.erp.app.shimmer.TinyTextShimmer +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +private fun AuditEventsLoadingItem() { + Column( + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + LimitedTextShimmer(modifier = Modifier.fillMaxWidth()) + LimitedTextShimmer(modifier = Modifier.fillMaxWidth()) + TinyTextShimmer() + LimitedTextShimmer() + SpacerLarge() + } +} + +@Composable +fun AuditEventsLoading() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium) + .shimmer() + ) { + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + AuditEventsLoadingItem() + } +} + +@LightDarkPreview +@Composable +fun AuditEventsLoadingPreview() { + PreviewAppTheme { + AuditEventsLoading() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/Avatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/Avatar.kt new file mode 100644 index 00000000..6995de4f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/Avatar.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AddAPhoto +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun Avatar( + modifier: Modifier, + emptyIcon: ImageVector, + profile: ProfilesUseCaseData.Profile, + active: Boolean = false, + iconModifier: Modifier +) { + val currentSelectedColors = profileColor(profileColorNames = profile.color) + Box( + modifier = modifier + .fillMaxSize() + .aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + Surface( + modifier = Modifier + .fillMaxSize(), + shape = CircleShape, + color = currentSelectedColors.backGroundColor, + border = if (active) BorderStroke(SizeDefaults.quarter, currentSelectedColors.borderColor) else null + ) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ChooseAvatar( + image = profile.image, + profileColor = profile.color.color(), + emptyIcon = emptyIcon, + modifier = iconModifier, + avatar = profile.avatar + ) + } + } + } +} + +@Composable +fun ChooseAvatar( + modifier: Modifier = Modifier, + useSmallImages: Boolean? = false, + image: ByteArray?, + profileColor: ProfileColor, + emptyIcon: ImageVector, + avatar: ProfilesData.Avatar +) { + when (avatar) { + ProfilesData.Avatar.PersonalizedImage -> { + if (image != null) { + BitmapImage( + modifier = Modifier.background(profileColor.backGroundColor), + image = image + ) + } else { + Icon( + imageVector = emptyIcon, + tint = AppTheme.colors.neutral600, + contentDescription = null + ) + } + } + + else -> { + val imageResource = extractImageResource(useSmallImages, avatar) + if (imageResource == 0) { + Icon( + modifier = modifier.background(profileColor.backGroundColor), + imageVector = emptyIcon, + tint = AppTheme.colors.neutral600, + contentDescription = null + ) + } else { + Image( + modifier = Modifier + .testTag(avatar.name) + .fillMaxSize() + .background(profileColor.backGroundColor), + painter = painterResource(id = imageResource), + contentDescription = null + ) + } + } + } +} + +@Suppress("ComplexMethod") +@Composable +private fun extractImageResource( + useSmallImages: Boolean? = false, + figure: ProfilesData.Avatar +) = if (useSmallImages == true) { + when (figure) { + ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_small_portrait + ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_small_portrait + ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_small_portrait + ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_small_portrait + ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_small_portrait + ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_small_portrait + ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_small_portrait + ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_small_portrait + ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_small_portrait + ProfilesData.Avatar.Baby -> R.drawable.baby_small_portrait + ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_small_portrait + ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_small_portrait + ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_small_portrait + else -> 0 + } +} else { + when (figure) { + ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_portrait + ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_portrait + ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_portrait + ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_portrait + ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_portrait + ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_portrait + ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_portrait + ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_portrait + ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_portrait + ProfilesData.Avatar.Baby -> R.drawable.baby_portrait + ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_portrait + ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_portrait + ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_portrait + else -> 0 + } +} + +@Preview +@Composable +private fun AvatarPreview() { + AppTheme { + Avatar( + modifier = Modifier.size(SizeDefaults.fourfoldAndHalf), + profile = ProfilesUseCaseData.Profile( + id = "", + name = "", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), + isActive = false, + color = ProfilesData.ProfileColorNames.SUN_DEW, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null, + lastAuthenticated = null, + ssoTokenScope = null + ), + active = false, + iconModifier = Modifier.size(SizeDefaults.doubleHalf), + emptyIcon = Icons.Rounded.AddAPhoto + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/BitmapProfileImage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/BitmapProfileImage.kt new file mode 100644 index 00000000..129fb59b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/BitmapProfileImage.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +internal fun CircularBitmapImage( + modifier: Modifier = Modifier, + image: Bitmap, + size: Dp = SizeDefaults.twentyfold +) { + Image( + painter = BitmapPainter(image.asImageBitmap()), + contentDescription = null, + modifier = modifier.size(size), + contentScale = ContentScale.Crop + ) +} + +@Composable +internal fun BitmapImage( + modifier: Modifier = Modifier, + image: ByteArray? +) { + val bitmap by produceState(initialValue = null, image) { + value = image?.let { img -> + BitmapFactory.decodeByteArray(img, 0, img.size).asImageBitmap() + } + } + + bitmap?.let { + Image( + modifier = modifier.fillMaxSize(), + bitmap = it, + contentDescription = null + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/DeleteDeviceDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/DeleteDeviceDialog.kt new file mode 100644 index 00000000..2f381dd3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/DeleteDeviceDialog.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.annotatedStringBold +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +private data class DeleteDeviceDialogParams( + val title: String, + val info: AnnotatedString +) + +@Composable +fun DeleteDeviceDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onClickAction: (PairedDevice) -> Unit +) { + event.listen { device -> + dialogScaffold.show { + val dialogParams = if (device.isCurrentDevice) { + DeleteDeviceDialogParams( + title = stringResource(R.string.paired_devices_delete_this_title), + info = annotatedStringResource( + R.string.paired_devices_delete_this_description, + annotatedStringBold(device.name) + ) + ) + } else { + DeleteDeviceDialogParams( + title = stringResource(R.string.paired_devices_delete_title), + info = annotatedStringResource( + R.string.paired_devices_delete_description, + annotatedStringBold(device.name) + ) + ) + } + DeleteDeviceDialog( + dialogParams = dialogParams, + onClickDismiss = { it.dismiss() }, + onClickAction = { + onClickAction(device) + it.dismiss() + } + ) + } + } +} + +@Composable +private fun DeleteDeviceDialog( + dialogParams: DeleteDeviceDialogParams, + onClickDismiss: () -> Unit, + onClickAction: () -> Unit +) { + ErezeptAlertDialog( + title = dialogParams.title, + bodyText = dialogParams.info.text, + confirmText = stringResource(R.string.paired_devices_delete_remove), + dismissText = stringResource(R.string.paired_devices_delete_cancel), + onDismissRequest = onClickDismiss, + onConfirmRequest = onClickAction + ) +} + +@Composable +@LightDarkPreview +fun DeleteDeviceDialogPreview() { + PreviewAppTheme { + DeleteDeviceDialog( + dialogParams = DeleteDeviceDialogParams( + title = stringResource(R.string.paired_devices_delete_this_title), + info = annotatedStringResource( + R.string.paired_devices_delete_description, + annotatedStringBold("Pixel 10") + ) + ), + onClickDismiss = {}, + onClickAction = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileBackgroundColorComponent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileBackgroundColorComponent.kt new file mode 100644 index 00000000..d5e17817 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileBackgroundColorComponent.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.ui.screens.ColorPicker +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun ColumnScope.ProfileBackgroundColorComponent( + color: ProfilesData.ProfileColorNames, + onColorPicked: (ProfilesData.ProfileColorNames) -> Unit +) { + SpacerXXLarge() + SpacerMedium() + Text( + modifier = Modifier.padding(PaddingDefaults.Tiny), + color = AppTheme.colors.neutral900, + text = stringResource(R.string.edit_profile_background_color), + style = AppTheme.typography.h6 + ) + SpacerLarge() + ColorPicker( + modifier = Modifier.padding(top = PaddingDefaults.Tiny), + profileColorName = color, + onSelectProfileColor = onColorPicked + ) +} + +@LightDarkPreview +@Composable +fun ProfileBackgroundColorComponentPreview() { + PreviewAppTheme { + Column { + ProfileBackgroundColorComponent( + color = ProfilesData.ProfileColorNames.BLUE_MOON, + onColorPicked = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileColor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileColor.kt new file mode 100644 index 00000000..94b5c179 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileColor.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.theme.AppTheme + +@Immutable +data class ProfileColor(val textColor: Color, val colorName: String, val backGroundColor: Color, val borderColor: Color) + +@Composable +fun ProfilesData.ProfileColorNames.color(): ProfileColor { + return profileColor(this) +} + +@Composable +fun profileColor(profileColorNames: ProfilesData.ProfileColorNames): ProfileColor { + return when (profileColorNames) { + ProfilesData.ProfileColorNames.SPRING_GRAY -> ProfileColor( + textColor = AppTheme.colors.neutral700, + colorName = stringResource(R.string.profile_color_name_gray), + backGroundColor = AppTheme.colors.neutral200, + borderColor = AppTheme.colors.neutral400 + ) + + ProfilesData.ProfileColorNames.SUN_DEW -> ProfileColor( + textColor = AppTheme.colors.yellow700, + colorName = stringResource(R.string.profile_color_sun_dew), + backGroundColor = AppTheme.colors.yellow200, + borderColor = AppTheme.colors.yellow400 + ) + + ProfilesData.ProfileColorNames.PINK -> ProfileColor( + textColor = AppTheme.colors.red700, + colorName = stringResource(R.string.profile_color_name_pink), + backGroundColor = AppTheme.colors.red200, + borderColor = AppTheme.colors.red400 + ) + + ProfilesData.ProfileColorNames.TREE -> ProfileColor( + textColor = AppTheme.colors.green700, + colorName = stringResource(R.string.profile_color_name_tree), + backGroundColor = AppTheme.colors.green200, + borderColor = AppTheme.colors.green400 + ) + + ProfilesData.ProfileColorNames.BLUE_MOON -> ProfileColor( + textColor = AppTheme.colors.primary700, + colorName = stringResource(R.string.profile_color_name_moon), + backGroundColor = AppTheme.colors.primary200, + borderColor = AppTheme.colors.primary400 + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileImageSelectorDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileImageSelectorDialog.kt new file mode 100644 index 00000000..b3c19151 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/components/ProfileImageSelectorDialog.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.components + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.Center +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErezeptText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +// to show dialog with event +@Composable +internal fun ProfileImageSelectorDialog( + composableEvent: ComposableEvent, + dialogScaffold: DialogScaffold, + onPickEmojiImage: () -> Unit, + onPickPersonalizedImage: () -> Unit, + onPickCamera: () -> Unit +) { + composableEvent.listen { + dialogScaffold.show { + ProfileImageSelectorDialog( + onPickEmoji = { + onPickEmojiImage() + it.dismiss() + }, + onPickCamera = { + onPickCamera() + it.dismiss() + }, + onPickPersonalizedImage = { + onPickPersonalizedImage() + it.dismiss() + }, + onDismiss = { + it.dismiss() + } + ) + } + } +} + +// to show dialog without an event, but just click +internal fun showProfileImageSelectorDialog( + dialogScaffold: DialogScaffold, + onPickEmojiImage: () -> Unit, + onPickPersonalizedImage: () -> Unit, + onPickCamera: () -> Unit +) { + dialogScaffold.show { + ProfileImageSelectorDialog( + onPickEmoji = { + onPickEmojiImage() + it.dismiss() + }, + onPickCamera = { + onPickCamera() + it.dismiss() + }, + onPickPersonalizedImage = { + onPickPersonalizedImage() + it.dismiss() + }, + onDismiss = { + it.dismiss() + } + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun ProfileImageSelectorDialog( + onPickEmoji: () -> Unit, + onPickPersonalizedImage: () -> Unit, + onPickCamera: () -> Unit, + onDismiss: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.profile_image_selector_dialog_title), + body = stringResource(R.string.profile_image_selector_dialog_body), + bodyAlignment = ErezeptText.TextAlignment.Center, + onDismissRequest = onDismiss, + buttons = { + ProfileImageSelectorButton( + text = stringResource(R.string.profile_image_selector_dialog_gallery_button), + onClick = onPickPersonalizedImage + ) + HorizontalDivider(modifier = Modifier.alpha(0.3f)) + ProfileImageSelectorButton( + text = stringResource(R.string.profile_image_selector_dialog_camera_button), + onClick = onPickCamera + ) + HorizontalDivider(modifier = Modifier.alpha(0.3f)) + ProfileImageSelectorButton( + text = stringResource(R.string.profile_image_selector_dialog_emoji_button), + onClick = onPickEmoji + ) + HorizontalDivider(modifier = Modifier.alpha(0.3f)) + ProfileImageSelectorButton( + text = stringResource(R.string.profile_image_selector_dialog_cancel_button), + onClick = onDismiss + ) + SpacerMedium() + } + ) +} + +@Suppress("MagicNumber") +@Composable +private fun ProfileImageSelectorButton( + text: String, + tint: Color = AppTheme.colors.primary600, + onClick: () -> Unit +) { + TextButton(onClick = onClick) { + Center { + Text( + color = tint, + textAlign = TextAlign.Center, + text = text + ) + } + } +} + +@LightDarkPreview +@Composable +fun ProfileImageSelectorDialogPreview() { + PreviewAppTheme { + ProfileImageSelectorDialog( + onPickEmoji = {}, + onPickPersonalizedImage = {}, + onPickCamera = {}, + onDismiss = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PairedDevicesPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PairedDevicesPreviewParameterProvider.kt new file mode 100644 index 00000000..b295da32 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PairedDevicesPreviewParameterProvider.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.CannotLoadPairedDevicesError +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.NoInternetError +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.UserNotLoggedInWithBiometricsError +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import de.gematik.ti.erp.app.utils.uistate.UiState + +class PairedDevicesPreviewParameterProvider : PreviewParameterProvider>> { + override val values: Sequence>> + get() = sequenceOf( + UiState.Loading(), + UiState.Empty(), + UiState.Error(UserNotLoggedInWithBiometricsError), + UiState.Error(CannotLoadPairedDevicesError), + UiState.Error(NoInternetError), + pairedDevices + ) +} + +private val pairedDevices = UiState.Data( + data = listOf( + PairedDevice( + name = "Tony StarksPhone", + alias = "IronPhone", + connectedOn = "2021-08-01", + isCurrentDevice = true + ), + PairedDevice( + name = "Thor OdinsonsTablet", + alias = "HammerTab", + connectedOn = "2021-07-15", + isCurrentDevice = false + ), + PairedDevice( + name = "Peter ParkersLaptop", + alias = "SpideyWeb", + connectedOn = "2021-09-10", + isCurrentDevice = false + ), + PairedDevice( + name = "Steve RogersWatch", + alias = "CapTime", + connectedOn = "2021-10-05", + isCurrentDevice = true + ), + PairedDevice( + name = "Bruce BannersSmartwatch", + alias = "HulkSmashTime", + connectedOn = "2021-11-20", + isCurrentDevice = false + ) + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PreviewProfileStates.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PreviewProfileStates.kt new file mode 100644 index 00000000..16d6a981 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/PreviewProfileStates.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewData.PharmacyOrders +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewData.emptyPharmacyOrders +import de.gematik.ti.erp.app.profiles.model.ProfileCombinedData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.datetime.Clock + +class PrescriptionPreviewParameterProvider : PreviewParameterProvider> { + + override val values: Sequence> + get() = sequenceOf( + PharmacyOrders, + emptyPharmacyOrders + ) +} + +val gkvProfile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + lastAuthenticated = Clock.System.now(), + ssoTokenScope = null, + insurance = ProfileInsuranceInformation( + insurantName = "Max Mustermann", + insuranceName = "TK", + insuranceIdentifier = "123456789", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true +) + +val emptyGkvProfile = ProfilesUseCaseData.Profile( + id = "", + name = "", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + lastAuthenticated = null, + ssoTokenScope = null, + insurance = ProfileInsuranceInformation( + insurantName = "", + insuranceName = "", + insuranceIdentifier = "", + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), + isActive = false +) + +val gkvProfileState = UiState( + data = ProfileCombinedData( + selectedProfile = gkvProfile, + profiles = listOf( + gkvProfile + ) + ) +) + +val pkvProfile = gkvProfile.copy( + insurance = ProfileInsuranceInformation( + insurantName = "Max Mustermann", + insuranceName = "TK", + insuranceIdentifier = "123456789", + insuranceType = ProfilesUseCaseData.InsuranceType.PKV + ) +) + +val pkvProfileState = UiState( + data = ProfileCombinedData( + selectedProfile = pkvProfile, + profiles = listOf( + pkvProfile + ) + ) +) + +val profileMissingImage = gkvProfile.copy(avatar = ProfilesData.Avatar.PersonalizedImage) + +val profileMissingImageState = UiState( + data = ProfileCombinedData( + selectedProfile = profileMissingImage, + profiles = listOf( + profileMissingImage + ) + ) +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/ProfileStatePreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/ProfileStatePreviewParameterProvider.kt new file mode 100644 index 00000000..1b416619 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/preview/ProfileStatePreviewParameterProvider.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.profiles.model.ProfileCombinedData +import de.gematik.ti.erp.app.profiles.ui.preview.ProfilePreviewParameterData.emptyGkvProfileError +import de.gematik.ti.erp.app.profiles.ui.preview.ProfilePreviewParameterData.existingGkvProfile +import de.gematik.ti.erp.app.profiles.ui.preview.ProfilePreviewParameterData.newGkvProfile +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.uistate.UiState + +class ProfileStatePreviewParameterProvider : PreviewParameterProvider> { + + override val values = sequenceOf( + UiState.Loading(), + UiState.Empty(), + UiState.Error(Throwable("Error")), + gkvProfileState, + pkvProfileState, + profileMissingImageState + ) +} + +data class ProfileEditData( + val profile: ProfilesUseCaseData.Profile, + val initialDuplicated: Boolean = false, + val initialHasUserInteracted: Boolean = false +) + +class ProfilePreviewParameterProvider : PreviewParameterProvider { + + override val values: Sequence = sequenceOf( + newGkvProfile, + existingGkvProfile, + emptyGkvProfileError + ) +} + +object ProfilePreviewParameterData { + + var newGkvProfile: ProfileEditData = ProfileEditData( + gkvProfile, + false + ) + var existingGkvProfile = ProfileEditData( + gkvProfile, + true, + true + ) + + var emptyGkvProfileError: ProfileEditData = ProfileEditData( + emptyGkvProfile, + false, + true + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileAuditEventsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileAuditEventsScreen.kt new file mode 100644 index 00000000..76b251fa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileAuditEventsScreen.kt @@ -0,0 +1,339 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.ui.components.AuthenticationFailureDialog +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberAuditEventsController +import de.gematik.ti.erp.app.profiles.ui.components.AuditEventsLoading +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.pulltorefresh.PullToRefresh +import de.gematik.ti.erp.app.pulltorefresh.extensions.trigger +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Center +import de.gematik.ti.erp.app.utils.compose.ConnectBottomBar +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.phrasedDateString +import kotlinx.coroutines.flow.collectLatest +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime + +@Requirement( + "O.Auth_6#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Screen displaying the audit events." +) +class ProfileAuditEventsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val listState = rememberLazyListState() + val pullToRefreshState = rememberPullToRefreshState() + val intentHandler = LocalIntentHandler.current + + val profileId = remember { ProfileRoutes.getProfileId(navBackStackEntry) } + + val auditController = rememberAuditEventsController(profileId) + + val auditEvents = auditController.auditEvents.collectAsLazyPagingItems() + val isSsoTokenValid by auditController.isSsoTokenValidForSelectedProfile.collectAsStateWithLifecycle() + + navBackStackEntry.onReturnAction(ProfileRoutes.ProfileAuditEventsScreen) { + auditController.refreshCombinedProfile() + } + + with(auditController) { + refreshStartedEvent.listen { + pullToRefreshState.endRefresh() + } + showCardWallEvent.listen { id -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(id)) + } + showCardWallWithFilledCanEvent.listen { cardWallData -> + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = cardWallData.profileId, + can = cardWallData.can + ) + ) + } + showGidEvent.listen { gidData -> + navController.navigate( + CardWallRoutes.CardWallIntroScreen.pathWithGid( + profileIdentifier = gidData.profileId, + gidEventData = gidData + ) + ) + } + } + + AuthenticationFailureDialog( + event = auditController.showAuthenticationErrorDialog, + dialogScaffold = dialog + ) + + LaunchedEffect(Unit) { + intentHandler.gidSuccessfulIntent.collectLatest { + auditController.refreshCombinedProfile() + auditController.refreshAuditEvents() + } + + if (isSsoTokenValid) { + // refresh audit events + auditController.refreshAuditEvents() + } + } + + with(pullToRefreshState) { + trigger( + onStartRefreshing = { startRefresh() }, + block = { + if (isSsoTokenValid) { + auditController.refreshAuditEvents() + } + }, + onNavigation = { + if (!isSsoTokenValid) { + auditController.chooseAuthenticationMethod(profileId) + } + } + + ) + } + + AuditEventsScaffold( + listState = listState, + pullToRefreshState = pullToRefreshState, + isSsoTokenValid = isSsoTokenValid, + auditEvents = auditEvents, + onBack = { navController.popBackStack() }, + onInvalidSsoToken = { auditController.chooseAuthenticationMethod(profileId) } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AuditEventsScaffold( + listState: LazyListState, + pullToRefreshState: PullToRefreshState, + isSsoTokenValid: Boolean, + auditEvents: LazyPagingItems, + onInvalidSsoToken: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.Profile.AuditEvents.AuditEventsScreen), + listState = listState, + topBarTitle = stringResource(R.string.autitEvents_headline), + onBack = onBack, + bottomBar = { + if (!isSsoTokenValid) { + ConnectBottomBar( + infoText = stringResource(R.string.audit_events_connect_info) + ) { + onInvalidSsoToken() + } + } + }, + navigationMode = NavigationBarMode.Back + ) { _ -> + RefreshAuditEventsContent( + listState = listState, + pullToRefreshState = pullToRefreshState, + isSsoTokenValid = isSsoTokenValid, + auditEvents = auditEvents + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RefreshAuditEventsContent( + listState: LazyListState, + pullToRefreshState: PullToRefreshState, + isSsoTokenValid: Boolean, + auditEvents: LazyPagingItems +) { + Box( + Modifier + .fillMaxSize() + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + state = listState + ) { + auditEvents.apply { + AuditEventsScreenContent(this) + + when { + loadState.isLoadingDataOnRefresh() -> ShimmerLoadingAuditEvents() + loadState.isLoadingDataOnAppend() -> LastItemLoadingIndicator() + loadState.isErrorState() -> AuditEventsEmptyScreenContent(isSsoTokenValid) + loadState.isErrorWithEndOfPaginationReached() -> AuditEventsEmptyScreenContent(isSsoTokenValid) + else -> ShimmerLoadingAuditEvents() + } + } + } + PullToRefresh( + modifier = Modifier.align(Alignment.TopCenter), + pullToRefreshState = pullToRefreshState + ) + } +} + +@Suppress("FunctionName") +private fun LazyListScope.ShimmerLoadingAuditEvents() { + item { + AuditEventsLoading() + } +} + +@Suppress("FunctionName") +private fun LazyListScope.LastItemLoadingIndicator() { + item { + Center { + CircularProgressIndicator() + } + } +} + +@Suppress("FunctionName") +internal fun LazyListScope.AuditEventsEmptyScreenContent( + isSsoTokenValid: Boolean +) { + item { + Column( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + stringResource(R.string.no_audit_events_header), + modifier = Modifier.testTag(TestTag.Profile.AuditEvents.NoAuditEventHeader), + style = AppTheme.typography.subtitle1 + ) + SpacerSmall() + Text( + text = if (isSsoTokenValid) { + stringResource(R.string.no_audit_events_empty_protocol_list_info) + } else { + stringResource(R.string.no_audit_events_not_logged_in_protocol_list_info) + }, + style = AppTheme.typography.body2l, + modifier = Modifier.testTag(TestTag.Profile.AuditEvents.NoAuditEventInfo), + textAlign = TextAlign.Center + ) + } + } +} + +@Requirement( + "O.Auth_6#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "UI component to display the audit events." +) +@Suppress("FunctionName") +internal fun LazyListScope.AuditEventsScreenContent( + auditEvents: LazyPagingItems +) { + items( + count = auditEvents.itemCount, + key = auditEvents.itemKey { "${it.auditId}-${it.timestamp}-${it.uuid}}".hashCode() } + ) { index -> + val auditEvent = auditEvents[index] + auditEvent?.let { + Column( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .testTag(TestTag.Profile.AuditEvents.AuditEvent) + ) { + Text(auditEvent.description, style = AppTheme.typography.body2) + + val timestamp = remember { + auditEvent.timestamp + .toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime() + } + Text( + phrasedDateString(date = timestamp), + style = AppTheme.typography.body2l + ) + } + } + } +} + +private fun CombinedLoadStates.isLoadingDataOnRefresh() = refresh is LoadState.Loading + +private fun CombinedLoadStates.isLoadingDataOnAppend() = append is LoadState.Loading + +private fun CombinedLoadStates.isErrorWithEndOfPaginationReached() = + append is LoadState.NotLoading && append.endOfPaginationReached + +private fun CombinedLoadStates.isErrorState() = refresh is LoadState.Error || append is LoadState.Error diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditNameBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditNameBottomSheetScreen.kt new file mode 100644 index 00000000..54a42209 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditNameBottomSheetScreen.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileEditNameController +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.ui.preview.ProfileEditData +import de.gematik.ti.erp.app.profiles.ui.preview.ProfilePreviewParameterProvider +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.FullScreenLoadingIndicator +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.isKeyboardVisible +import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty +import de.gematik.ti.erp.app.utils.compose.Center as ComposeCenter + +class ProfileEditNameBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = false) { + @Composable + override fun Content() { + val profileId = remember { navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID) } + val controller = rememberProfileEditNameController(profileId) + + val combinedProfileData by controller.combinedProfile.collectAsStateWithLifecycle() + + val keyboardController = LocalSoftwareKeyboardController.current + + UiStateMachine( + state = combinedProfileData, + onEmpty = { + ComposeCenter { + ErrorScreenComponent(noMaxSize = true) + } + }, + onLoading = { + FullScreenLoadingIndicator() + }, + onError = { + ComposeCenter { + ErrorScreenComponent(noMaxSize = true) + } + } + ) { data -> + ProfileEditNameBottomSheetScreenContent( + existingProfiles = data.profiles, + profileToEdit = data.selectedProfile, + keyboardController = keyboardController, + addProfile = profileId == null, + onUpdate = { _, newName -> + controller.updateProfileName(newName) + }, + onAdd = { newName -> + controller.addNewProfile(newName) + }, + onCancel = { + navController.popBackStack() + } + ) + } + } +} + +/** + * @param profileToEdit is used to get the profile to edit + * @param existingProfiles: List is used to get the list of existing profiles + * @param keyboardController: SoftwareKeyboardController is used to control the keyboard + * @param addProfile: Boolean is used to differentiate between adding a new profile and editing an existing one + * @param onCancel: () -> Unit is used to dismiss the bottomsheet + */ +@Suppress("CyclomaticComplexMethod") +@Composable +fun ProfileEditNameBottomSheetScreenContent( + existingProfiles: List, + profileToEdit: ProfilesUseCaseData.Profile?, + keyboardController: SoftwareKeyboardController?, + addProfile: Boolean = false, + onUpdate: (ProfileIdentifier, String) -> Unit, + onAdd: (String) -> Unit, + onCancel: () -> Unit, + initialTextValue: TextFieldValue = TextFieldValue( + text = profileToEdit?.name ?: "", + selection = TextRange((profileToEdit?.name ?: "").length) + ), + initialDuplicated: Boolean = false, + initialHasUserInteracted: Boolean = false +) { + val view = LocalView.current + val focusRequester = remember { FocusRequester() } + + var textValue by remember { mutableStateOf(initialTextValue) } + var duplicated by remember { mutableStateOf(initialDuplicated) } + var isNewProfile by remember { mutableStateOf(initialTextValue.text.isEmpty()) } + var hasUserInteracted by remember { mutableStateOf(initialHasUserInteracted) } + + val showError by remember(duplicated, textValue, isNewProfile, hasUserInteracted) { + mutableStateOf((duplicated || textValue.text.isEmpty()) && hasUserInteracted) + } + + val onSave = { + if (addProfile) { + onAdd(textValue.text) + } else { + profileToEdit?.let { + onUpdate(it.id, textValue.text) + } + } + onCancel() + keyboardController?.hide() + } + + DisposableEffect(profileToEdit) { + focusRequester.requestFocus() + if (!view.isKeyboardVisible) { + keyboardController?.show() + } + onDispose { + keyboardController?.hide() + } + } + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SpacerLarge() + ErezeptOutlineText( + modifier = Modifier + .testTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) + .focusRequester(focusRequester), + shape = RoundedCornerShape(SizeDefaults.one), + value = textValue, + singleLine = true, + onValueChange = { changedValue -> + isNewProfile = false // once the user starts typing, it is no longer a new profile for this composable + hasUserInteracted = true + textValue = TextFieldValue( + text = changedValue.text.sanitizeProfileName(), + selection = changedValue.selection, + composition = changedValue.composition + ) + val isExistingText = textValue.text.trim() == profileToEdit?.name?.trim() + val isExistingProfileName = existingProfiles.containsProfileWithName(textValue.text) + duplicated = isExistingProfileName || isExistingText + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + capitalization = KeyboardCapitalization.Sentences + ), + keyboardActions = KeyboardActions { + if (!duplicated && textValue.text.isNotEmpty()) { + onSave() + } + }, + placeholder = { Text(stringResource(R.string.profile_edit_name_place_holder)) }, + isError = showError, + trailingIcon = { + Crossfade( + label = "", + targetState = textValue.text.isNotEmpty() + ) { expectedState -> + if (expectedState) { + IconButton( + onClick = { + textValue = TextFieldValue( + text = "", + selection = TextRange(0) + ) + duplicated = false + hasUserInteracted = true + } + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + } + } + } + ) + AnimatedVisibility(showError) { + Text( + text = when { + textValue.text.isNotNullOrEmpty() && duplicated -> stringResource( + R.string.edit_profile_duplicated_profile_name + ) + else -> stringResource(id = R.string.edit_profile_empty_profile_name) + }, + color = AppTheme.colors.red600, + style = AppTheme.typography.caption1, + modifier = Modifier.padding( + top = PaddingDefaults.Medium, + start = PaddingDefaults.Medium + ) + ) + } + SpacerLarge() + PrimaryButton( + modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton), + enabled = !duplicated && textValue.text.isNotEmpty(), + onClick = { + onSave() + } + ) { + Text(stringResource(R.string.profile_bottom_sheet_save)) + } + } +} + +@LightDarkPreview +@Composable +fun ProfileEditNameBottomSheetScreenContentPreview( + @PreviewParameter(ProfilePreviewParameterProvider::class) editProfilePreviewParameter: ProfileEditData +) { + PreviewAppTheme { + val existingProfiles = listOf(editProfilePreviewParameter.profile) + val profileToEdit = editProfilePreviewParameter.profile + + ProfileEditNameBottomSheetScreenContent( + existingProfiles = existingProfiles, + profileToEdit = profileToEdit, + keyboardController = null, + addProfile = false, + onUpdate = { _, _ -> }, + onAdd = { }, + onCancel = { }, + initialTextValue = TextFieldValue( + text = profileToEdit.name, + selection = TextRange(profileToEdit.name.length) + ), + initialDuplicated = editProfilePreviewParameter.initialDuplicated, + initialHasUserInteracted = editProfilePreviewParameter.initialHasUserInteracted + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureBottomSheetScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureBottomSheetScreen.kt new file mode 100644 index 00000000..21ec81f6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureBottomSheetScreen.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.navigation.BottomSheetScreen +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileEditPictureController +import de.gematik.ti.erp.app.profiles.ui.components.ProfileBackgroundColorComponent +import de.gematik.ti.erp.app.profiles.ui.components.ProfileImageSelectorDialog +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.CenterColumn +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.FullScreenLoadingIndicator +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog + +class ProfileEditPictureBottomSheetScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : BottomSheetScreen(forceToMaxHeight = true) { + @Composable + override fun Content() { + val dialog = LocalDialog.current + val profileId = remember { navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID) } + val controller = rememberProfileEditPictureController(profileId) + val profileData by controller.profile.collectAsStateWithLifecycle() + val imageTypeDialogEvent by lazy { ComposableEvent() } + + ProfileImageSelectorDialog( + composableEvent = imageTypeDialogEvent, + dialogScaffold = dialog, + onPickEmojiImage = { + profileId?.let { profileId -> + navController.navigate( + ProfileRoutes.ProfileImageEmojiScreen.path(profileId) + ) + } + }, + onPickPersonalizedImage = { + profileId?.let { profileId -> + navController.navigate( + ProfileRoutes.ProfileImageCropperScreen.path(profileId) + ) + } + }, + onPickCamera = { + profileId?.let { profileId -> + navController.navigate( + ProfileRoutes.ProfileImageCameraScreen.path(profileId) + ) + } + } + ) + + UiStateMachine( + state = profileData, + onError = { + ErrorScreenComponent() + }, + onLoading = { + FullScreenLoadingIndicator() + }, + onContent = { profile -> + ProfileEditAvatarScreenContent( + profile = profile, + clearPersonalizedImage = { + controller.clearPersonalizedImage() + }, + onPickPersonalizedImage = { + imageTypeDialogEvent.trigger() + }, + onSelectAvatar = { + controller.updateAvatar(it) + }, + onSelectProfileColor = { + controller.updateProfileColor(it) + } + ) + } + ) + } +} + +@Composable +private fun ProfileEditAvatarScreenContent( + profile: ProfilesUseCaseData.Profile, + clearPersonalizedImage: () -> Unit, + onPickPersonalizedImage: () -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit, + onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit +) { + var editableProfile by remember(profile.id) { mutableStateOf(profile) } + Column( + modifier = Modifier.wrapContentSize() + ) { + SpacerMedium() + ProfileImage(editableProfile) { + editableProfile = editableProfile.copy( + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) + clearPersonalizedImage() + } + + SpacerXXLarge() + AvatarPicker( + profile = editableProfile, + currentAvatar = editableProfile.avatar, + onPickPersonalizedImage = onPickPersonalizedImage, + onSelectAvatar = { + editableProfile = editableProfile.copy(avatar = it) + onSelectAvatar(it) + } + ) + SpacerSmall() + CenterColumn { + ProfileBackgroundColorComponent( + color = editableProfile.color, + onColorPicked = { + editableProfile = editableProfile.copy(color = it) + onSelectProfileColor(it) + } + ) + } + } +} + +@Composable +@LightDarkPreview +fun ProfileEditAvatarScreenContentPreview() { + PreviewAppTheme { + ProfileEditAvatarScreenContent( + profile = ProfilesUseCaseData.Profile( + id = "1", + name = "Max Mustermann", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = null, + ssoTokenScope = null, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ), + clearPersonalizedImage = {}, + onPickPersonalizedImage = {}, + onSelectAvatar = {}, + onSelectProfileColor = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureScreen.kt new file mode 100644 index 00000000..b08ef3b1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileEditPictureScreen.kt @@ -0,0 +1,526 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.rounded.AddAPhoto +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileEditPictureController +import de.gematik.ti.erp.app.profiles.ui.components.ChooseAvatar +import de.gematik.ti.erp.app.profiles.ui.components.ProfileBackgroundColorComponent +import de.gematik.ti.erp.app.profiles.ui.components.ProfileColor +import de.gematik.ti.erp.app.profiles.ui.components.ProfileImageSelectorDialog +import de.gematik.ti.erp.app.profiles.ui.components.color +import de.gematik.ti.erp.app.profiles.ui.components.profileColor +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.Center +import de.gematik.ti.erp.app.utils.compose.CenterColumn +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.circularBorder + +// todo: this is duplicated from EditProfilePicture.kt, needs to be combined into one view +class ProfileEditPictureScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val dialog = LocalDialog.current + val profileId = + remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID)) } + val imageTypeDialogEvent by lazy { ComposableEvent() } + val profilesController = rememberProfileController() + val profileEditPictureController = rememberProfileEditPictureController(profileId) + val profileData by profileEditPictureController.profile.collectAsStateWithLifecycle() + + ProfileImageSelectorDialog( + composableEvent = imageTypeDialogEvent, + dialogScaffold = dialog, + onPickEmojiImage = { + navController.navigate( + ProfileRoutes.ProfileImageEmojiScreen.path(profileId) + ) + }, + onPickPersonalizedImage = { + navController.navigate( + ProfileRoutes.ProfileImageCropperScreen.path(profileId) + ) + }, + onPickCamera = { + navController.navigate( + ProfileRoutes.ProfileImageCameraScreen.path(profileId) + ) + } + ) + + UiStateMachine( + state = profileData, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onError = { + Center { + Text("Error loading profile data") + } + }, + onContent = { selectedProfile -> + val listState = rememberLazyListState() + var editableProfile by remember(selectedProfile.image) { mutableStateOf(selectedProfile) } + Scaffold( + modifier = Modifier.imePadding(), + topBar = { + NavigationTopAppBar( + navigationMode = NavigationBarMode.Back, + title = stringResource(R.string.edit_profile_picture), + onBack = { navController.popBackStack() }, + actions = {} + ) + } + ) { paddingValues -> + LazyColumn( + state = listState, + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + item { + SpacerMedium() + ProfileImage(editableProfile) { + editableProfile = editableProfile.copy( + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) + profilesController.clearPersonalizedImage(selectedProfile.id) + } + } + item { + SpacerXXLarge() + AvatarPicker( + profile = editableProfile, + currentAvatar = editableProfile.avatar, + onPickPersonalizedImage = { + imageTypeDialogEvent.trigger() + }, + onSelectAvatar = { + editableProfile = editableProfile.copy(avatar = it) + profilesController.saveAvatarFigure(selectedProfile.id, it) + } + ) + } + item { + CenterColumn { + ProfileBackgroundColorComponent( + color = editableProfile.color + ) { + editableProfile = editableProfile.copy(color = it) + profilesController.updateProfileColor(selectedProfile, it) + } + } + } + } + } + } + ) + } +} + +@Composable +fun AvatarPicker( + profile: ProfilesUseCaseData.Profile, + currentAvatar: ProfilesData.Avatar?, + onPickPersonalizedImage: () -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit +) { + val listState = rememberLazyListState() + + LazyRow( + state = listState, + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + ProfilesData.Avatar.entries.forEachIndexed { index, figure -> + item { + AvatarSelector( + modifier = when (index) { + ProfilesData.Avatar.lastIndex -> Modifier.padding(end = PaddingDefaults.Small) + ProfilesData.Avatar.firstIndex -> Modifier.padding(start = PaddingDefaults.Small) + else -> Modifier + }, + figure = figure, + profile = profile.copy(image = null), + selected = figure == currentAvatar, + onPickPersonalizedImage = onPickPersonalizedImage, + onSelectAvatar = onSelectAvatar + ) + } + } + } +} + +@Composable +private fun AvatarSelector( + modifier: Modifier = Modifier, + profile: ProfilesUseCaseData.Profile, + figure: ProfilesData.Avatar, + selected: Boolean, + onPickPersonalizedImage: () -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit +) { + Surface( + modifier = modifier.size(SizeDefaults.tenfold), + shape = CircleShape, + border = if (selected) { + BorderStroke(SizeDefaults.fivefoldHalf, color = AppTheme.colors.primary600) + } else if (figure != ProfilesData.Avatar.PersonalizedImage) { + BorderStroke(SizeDefaults.eighth, color = AppTheme.colors.neutral300) + } else { + null + } + ) { + Row( + modifier = Modifier + .testTag(TestTag.Profile.EditProfileIcon.AvatarSelectorRow) + .background( + color = when (figure) { + ProfilesData.Avatar.PersonalizedImage -> AppTheme.colors.neutral100 + else -> AppTheme.colors.neutral025 + } + ) + .clickable(onClick = { + if (figure == ProfilesData.Avatar.PersonalizedImage) { + onPickPersonalizedImage() + onSelectAvatar(figure) + } + onSelectAvatar(figure) + }), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + ChooseAvatar( + useSmallImages = true, + emptyIcon = Icons.Rounded.AddAPhoto, + modifier = Modifier.size(SizeDefaults.triple), + image = profile.image, + profileColor = profile.color.color(), + avatar = figure + ) + } + } +} + +@Composable +fun ColorPicker( + modifier: Modifier = Modifier, + profileColorName: ProfilesData.ProfileColorNames, + onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit +) { + val currentSelectedColors = profileColor(profileColorNames = profileColorName) + + Column( + modifier = Modifier + .then(modifier) + .fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + ProfilesData.ProfileColorNames.entries.forEach { + val currentValueColors = profileColor(profileColorNames = it) + ColorSelector( + modifier = Modifier.testTag( + when (it) { + ProfilesData.ProfileColorNames.SPRING_GRAY -> + TestTag.Profile.EditProfileIcon.ColorSelectorSpringGrayButton + + ProfilesData.ProfileColorNames.SUN_DEW -> + TestTag.Profile.EditProfileIcon.ColorSelectorSunDewButton + + ProfilesData.ProfileColorNames.PINK -> + TestTag.Profile.EditProfileIcon.ColorSelectorPinkButton + + ProfilesData.ProfileColorNames.TREE -> + TestTag.Profile.EditProfileIcon.ColorSelectorTreeButton + + ProfilesData.ProfileColorNames.BLUE_MOON -> + TestTag.Profile.EditProfileIcon.ColorSelectorBlueMoonButton + } + ), + profileColorName = it, + selected = currentValueColors == currentSelectedColors, + onSelectColor = onSelectProfileColor + ) + } + } + SpacerMedium() + Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { + Text( + text = currentSelectedColors.colorName, + color = AppTheme.colors.neutral600, + style = AppTheme.typography.body2l + ) + } + } +} + +@Composable +private fun createProfileColor(colors: ProfilesData.ProfileColorNames): ProfileColor { + return profileColor(profileColorNames = colors) +} + +@Composable +private fun ColorSelector( + modifier: Modifier, + profileColorName: ProfilesData.ProfileColorNames, + selected: Boolean, + onSelectColor: (ProfilesData.ProfileColorNames) -> Unit +) { + val colors = createProfileColor(profileColorName) + val contentDescription = annotatedStringResource( + R.string.edit_profile_color_selected, + profileColorName.name + ).toString() + + Surface( + modifier = modifier + .size(40.dp) + .clip(CircleShape) + .clickable(onClick = { onSelectColor(profileColorName) }), + color = colors.backGroundColor + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (selected) { + Icon( + Icons.Outlined.Done, + contentDescription, + tint = colors.textColor, + modifier = Modifier.size(24.dp) + ) + } + } + } +} + +@Composable +fun ProfileImage( + selectedProfile: ProfilesUseCaseData.Profile, + onClickDeleteAvatar: () -> Unit +) { + val selectedColor = profileColor(profileColorNames = selectedProfile.color) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + .semantics(true) { + stateDescription = selectedProfile.color.name + } + ) { + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + Box( + modifier = Modifier + .size(SizeDefaults.twentyfold) + .clip(CircleShape) + .aspectRatio(1f) + .circularBorder(selectedColor.borderColor) + .background(selectedColor.backGroundColor), + contentAlignment = Alignment.Center + ) { + ChooseAvatar( + modifier = Modifier.size(SizeDefaults.fourfoldAndHalf), + image = selectedProfile.image, + profileColor = selectedProfile.color.color(), + emptyIcon = Icons.Rounded.PersonOutline, + avatar = selectedProfile.avatar + ) + } + if (!(selectedProfile.hasNoImageSelected())) { + @Suppress("MagicNumber") + Box( + modifier = Modifier + .size(32.dp) + .align(Alignment.TopEnd) + .offset((-8).dp, 4.dp) + .clip(CircleShape) + .aspectRatio(1f) + .background(AppTheme.colors.neutral050) + .border(1.dp, color = AppTheme.colors.neutral000, shape = RoundedCornerShape(16.dp)) + ) { + IconButton( + onClick = { + onClickDeleteAvatar() + } + ) { + Icon( + modifier = Modifier.testTag( + TestTag.Profile.EditProfileIcon.DeleteAvatarButton + ), + imageVector = Icons.Rounded.Delete, + tint = AppTheme.colors.neutral600, + contentDescription = null + ) + } + } + } + } + } +} + +@LightDarkPreview +@Composable +fun AvatarSelectorPreview() { + val profile = mockProfile() + PreviewAppTheme { + AvatarSelector( + profile = profile, + figure = profile.avatar, + selected = false, + onPickPersonalizedImage = {}, + onSelectAvatar = {} + ) + } +} + +@LightDarkPreview +@Composable +fun AvatarPickerPreview() { + val profile = mockProfile() + val currentAvatar = ProfilesData.Avatar.PersonalizedImage + val onPickPersonalizedImage: () -> Unit = {} + val onSelectAvatar: (ProfilesData.Avatar) -> Unit = {} + PreviewAppTheme { + AvatarPicker( + profile = profile, + currentAvatar = currentAvatar, + onPickPersonalizedImage = onPickPersonalizedImage, + onSelectAvatar = onSelectAvatar + ) + } +} + +@LightDarkPreview +@Composable +fun ColorPickerPreview() { + val profileColorName = ProfilesData.ProfileColorNames.SPRING_GRAY + val onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit = {} + PreviewAppTheme { + ColorPicker( + modifier = Modifier.fillMaxWidth(), + profileColorName = profileColorName, + onSelectProfileColor = onSelectProfileColor + ) + } +} + +@LightDarkPreview +@Composable +fun ProfileImagePreview() { + val profile = mockProfile() + val onClickDeleteAvatar: () -> Unit = {} + PreviewAppTheme { + ProfileImage( + selectedProfile = profile, + onClickDeleteAvatar = onClickDeleteAvatar + ) + } +} + +@Composable +fun mockProfile(): ProfilesUseCaseData.Profile { + return ProfilesUseCaseData.Profile( + id = "", + name = "", + avatar = ProfilesData.Avatar.BoyWithHealthCard, + color = ProfilesData.ProfileColorNames.PINK, + isActive = false, + insurance = de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation(), + lastAuthenticated = null, + ssoTokenScope = null, + image = null + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCameraScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCameraScreen.kt new file mode 100644 index 00000000..9a672e41 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCameraScreen.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.provider.Settings +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AddAPhoto +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.permissions.cameraPermission +import de.gematik.ti.erp.app.permissions.getBitmapFromCamera +import de.gematik.ti.erp.app.permissions.hasPermissionOrRationale +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileImagePersonalizedImageScreenController +import de.gematik.ti.erp.app.profiles.ui.components.CircularBitmapImage +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AccessToCameraDenied +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar +import de.gematik.ti.erp.app.utils.compose.erezeptButtonColors +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.greyCircularBorder + +class ProfileImageCameraScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + ProfileImageCameraScreenComponent( + navController = navController, + navBackStackEntry = navBackStackEntry + ) + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun ProfileImageCameraScreenComponent( + navController: NavController, + navBackStackEntry: NavBackStackEntry +) { + val profileId = remember { + requireNotNull( + navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID) + ) { "ProfileId is missing in ProfileImageCameraScreen" } + } + val controller = rememberProfileImagePersonalizedImageScreenController(profileId) + var imageBitmap by remember { mutableStateOf(null) } + val cameraPermissionState = rememberPermissionState(permission = cameraPermission) + val takePictureLauncher = getBitmapFromCamera { + imageBitmap = it + } + val permissionPresent by remember(cameraPermissionState.status) { + derivedStateOf { + cameraPermissionState.hasPermissionOrRationale() + } + } + + LaunchedEffect(Unit) { + when { + cameraPermissionState.status.isGranted -> takePictureLauncher.launch(null) + else -> cameraPermissionState.launchPermissionRequest() + } + } + + @Requirement( + "O.Data_8#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Image without metadata is saved here." + ) + ProfileImageCameraScreenScaffold( + takePhotoText = stringResource(R.string.profile_image_selector_take_photo), + savePhotoText = stringResource(R.string.profile_image_selector_save), + imageBitmap = imageBitmap, + onClick = { + when { + cameraPermissionState.status.isGranted -> takePictureLauncher.launch(null) + else -> cameraPermissionState.launchPermissionRequest() + } + }, + onSavePicture = { + imageBitmap?.let { + controller.updateProfileImageBitmap(it) + navController.popBackStack() + } + }, + onBack = { + navController.popBackStack() + }, + permissionPresent = permissionPresent + ) +} + +@Composable +private fun ProfileImageCameraScreenScaffold( + takePhotoText: String, + savePhotoText: String, + imageBitmap: Bitmap?, + onClick: () -> Unit, + onBack: () -> Unit, + onSavePicture: () -> Unit, + permissionPresent: Boolean +) { + val context = LocalContext.current + Scaffold( + topBar = { + NavigationTopAppBar( + navigationMode = NavigationBarMode.Close, + title = stringResource(R.string.profile_image_selector_title), + onBack = onBack + ) + }, + bottomBar = { + AnimatedVisibility(permissionPresent) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = PaddingDefaults.XXLargeMedium), + contentAlignment = Alignment.Center + ) { + Button( + modifier = Modifier.padding(PaddingDefaults.Medium), + enabled = permissionPresent, + colors = erezeptButtonColors(), + onClick = { + imageBitmap?.let { onSavePicture() } ?: run { onClick() } + } + ) { + Text( + text = when { + imageBitmap != null -> savePhotoText + else -> takePhotoText + } + ) + } + } + } + } + ) { + Column( + modifier = Modifier + .background(AppTheme.colors.neutral000) + .fillMaxSize() + .padding(it), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (!permissionPresent) { + SpacerMedium() + AccessToCameraDenied( + onClick = { + context.openSettingsAsNewActivity(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + }, + showTopBar = false, + showSettingsButton = true + ) + } else { + imageBitmap?.let { bitmap -> + CircularBitmapImage( + modifier = Modifier + .clip(CircleShape) + .greyCircularBorder() + .padding(vertical = PaddingDefaults.Medium), + image = bitmap, + size = SizeDefaults.thirtyEightfold + ) + } ?: run { + Image( + modifier = Modifier + .clip(CircleShape) + .padding(SizeDefaults.twentyfold) + .fillMaxSize(), + imageVector = Icons.Rounded.AddAPhoto, + colorFilter = ColorFilter.tint(AppTheme.colors.primary400), + contentDescription = null + + ) + } + } + } + } +} + +@LightDarkPreview +@Composable +internal fun ProfileImageCameraScreenNoPermissionPreview() { + PreviewAppTheme { + ProfileImageCameraScreenScaffold( + takePhotoText = "Take Photo", + savePhotoText = "Save Photo", + imageBitmap = null, + onClick = {}, + onSavePicture = {}, + onBack = {}, + permissionPresent = false + ) + } +} + +@LightDarkPreview +@Composable +internal fun ProfileImageCameraScreenWithPermissionPreview() { + PreviewAppTheme { + val context = LocalContext.current + ProfileImageCameraScreenScaffold( + imageBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.prescription), + takePhotoText = "Take Photo", + savePhotoText = "Save Photo", + onClick = {}, + onSavePicture = {}, + onBack = {}, + permissionPresent = true + ) + } +} + +@LightDarkPreview +@Composable +internal fun ProfileImageCameraScreenWithPermissionNoImagePreview() { + PreviewAppTheme { + ProfileImageCameraScreenScaffold( + imageBitmap = null, + takePhotoText = "Take Photo", + savePhotoText = "Save Photo", + onClick = {}, + onSavePicture = {}, + onBack = {}, + permissionPresent = true + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropperScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCropperScreen.kt similarity index 81% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropperScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCropperScreen.kt index 689282bf..40e74baa 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropperScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageCropperScreen.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.profiles.ui +package de.gematik.ti.erp.app.profiles.ui.screens import android.Manifest import android.content.Context @@ -31,6 +31,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextButton @@ -59,6 +60,8 @@ import com.canhub.cropper.CropImageView import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.permissions.removeMetadataFromBitmap +import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById @@ -74,10 +77,10 @@ class ProfileImageCropperScreen( ) : Screen() { @Composable override fun Content() { - val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.ProfileId)) } + val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID)) } val profilesController = rememberProfileController() - val profiles by profilesController.getProfilesState() - profiles.profileById(profileId)?.let { selectedProfile -> + val profiles by profilesController.getProfilesState2() + profiles?.profileById(profileId)?.let { selectedProfile -> val context = LocalContext.current val cropView = remember { CropImageView(context).apply { @@ -107,7 +110,12 @@ class ProfileImageCropperScreen( val imagePickerLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? -> uri?.let { - background = getOriginalBitMap(context, uri) + @Requirement( + "O.Data_8#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Metadata is removed from the bitmap before it is shown to the user to save as avatar." + ) + background = removeMetadataFromBitmap(getOriginalBitMap(context, uri)) cropView.setImageBitmap(background) } ?: run { navController.popBackStack() } } @@ -128,6 +136,7 @@ class ProfileImageCropperScreen( reqHeight = CROPPED_IMAGE_SIZE )?.let { profilesController.savePersonalizedProfileImage(selectedProfile.id, it) + profilesController.saveAvatarFigure(selectedProfile.id, ProfilesData.Avatar.PersonalizedImage) navController.popBackStack() } }) { @@ -172,6 +181,8 @@ class ProfileImageCropperScreen( } } } + } ?: run { + CircularProgressIndicator() } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageEmojiScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageEmojiScreen.kt new file mode 100644 index 00000000..c89736c7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileImageEmojiScreen.kt @@ -0,0 +1,612 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UnusedPrivateProperty", "UnusedPrivateMember") + +package de.gematik.ti.erp.app.profiles.ui.screens + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.net.Uri +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.content.MediaType +import androidx.compose.foundation.content.hasMediaType +import androidx.compose.foundation.content.receiveContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text2.BasicTextField2 +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.EmojiEmotions +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import coil.compose.AsyncImage +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.profiles.model.PictureDataType +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileImagePersonalizedImageScreenController +import de.gematik.ti.erp.app.profiles.ui.components.CircularBitmapImage +import de.gematik.ti.erp.app.profiles.ui.components.ProfileBackgroundColorComponent +import de.gematik.ti.erp.app.profiles.ui.components.color +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerXXXLarge +import de.gematik.ti.erp.app.utils.compose.CenterColumn +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar +import de.gematik.ti.erp.app.utils.compose.SaveButton +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.animatedCircularBorder +import de.gematik.ti.erp.app.utils.extensions.circularBorder +import de.gematik.ti.erp.app.utils.extensions.keyboardAsState +import de.gematik.ti.erp.app.utils.extensions.showKeyboardOnNotOpen +import io.github.aakira.napier.Napier +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.launch + +private const val TEXT_SIZE = 64f +private const val DIAMETER_X = 2f +private const val DIAMETER_Y = 2f +private const val SIZE_DP = 150 + +/** + * render as a full screen + */ +class ProfileImageEmojiScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + ProfileImageEmojiComponent( + navController = navController, + navBackStackEntry = navBackStackEntry + ) + } +} + +@Composable +fun ProfileImageEmojiComponent( + navController: NavController, + navBackStackEntry: NavBackStackEntry +) { + val profileId = remember { + requireNotNull( + navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID) + ) { "ProfileId is missing in ProfileImageEmojiScreen" } + } + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current + val snackbarHostState = remember { SnackbarHostState() } + val focusRequester = remember { FocusRequester() } + val scrollState = rememberScrollState() + + val emptySelectionString = stringResource(R.string.profile_image_selector_empty) + + var imageUri by remember { mutableStateOf(Uri.EMPTY) } + var textData by remember { mutableStateOf("") } + var pictureDataType by remember { mutableStateOf(PictureDataType.EMPTY) } + + val focusContentEvent by remember { mutableStateOf(ComposableEvent()) } + val coroutineScope = rememberCoroutineScope() + val isDarkMode = isSystemInDarkTheme() + + val controller = rememberProfileImagePersonalizedImageScreenController(profileId) + + val profile by controller.profile.collectAsStateWithLifecycle() + val isSamsungDevice = controller.isSamsungDevice() + + val keyboardState by keyboardAsState() + + // these two change the background color of the profile image + var colorName = profile?.color + val backgroundColor = colorName?.color()?.backGroundColor ?: Color.White + + val shouldAnimateImage by remember(keyboardState, pictureDataType) { + derivedStateOf { !keyboardState && pictureDataType == PictureDataType.EMPTY } + } + + val enableButton by remember(pictureDataType) { + derivedStateOf { pictureDataType != PictureDataType.EMPTY } + } + + focusContentEvent.listen { + coroutineScope.launch { + focusRequester.requestFocus() + // wait the keyboard to be open, if the focus worked and if not open the keyboard + awaitFrame() + keyboardController?.showKeyboardOnNotOpen(keyboardState) + } + } + + // Force keyboard to be open on start + LaunchedEffect(Unit) { + focusContentEvent.trigger() + } + + DisposableEffect(Unit) { + onDispose { + keyboardController?.hide() + } + } + + // If the user has not selected any content, show the warning + LaunchedEffect(textData, imageUri) { + if (textData.isBlank() && imageUri == Uri.EMPTY) { + pictureDataType = PictureDataType.EMPTY + } + } + + ProfileImageEmojiScreenContent( + snackbarHostState = snackbarHostState, + focusContentEvent = focusContentEvent, + focusRequester = focusRequester, + shouldAnimateImage = shouldAnimateImage, + isEnabled = enableButton, + scrollState = scrollState, + pictureDataType = pictureDataType, + imageUri = imageUri, + textData = textData, + profile = profile, + isSamsungDevice = isSamsungDevice, + onColorPicked = { color -> + colorName = color + controller.updateProfileColor(color) + }, + onTextContentReceived = { text -> + if (text.isNotBlank()) { + imageUri = Uri.EMPTY + pictureDataType = PictureDataType.TEXT + } + textData = text + }, + onImageContentReceived = { + textData = "" + pictureDataType = PictureDataType.IMAGE + imageUri = it + }, + onBack = { navController.popBackStack() }, + onSelect = { + when (pictureDataType) { + PictureDataType.IMAGE -> { + uriToBitmap(context, imageUri)?.let { bitmap -> + controller.updateProfileImageBitmap(bitmap) + } + } + + PictureDataType.TEXT -> { + createCircularBitmapFromText( + context = context, + text = textData, + isDarkMode = isDarkMode, + backgroundColor = backgroundColor + )?.let { bitmap -> + controller.updateProfileImageBitmap(bitmap) + } + } + + else -> { + // no-op + } + } + navController.popBackStack() + }, + onEmpty = { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = emptySelectionString, + withDismissAction = true, + duration = SnackbarDuration.Long + ) + } + } + ) +} + +@Suppress("MagicNumber", "CyclomaticComplexMethod", "LongParameterList") +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ProfileImageEmojiScreenContent( + snackbarHostState: SnackbarHostState, + focusRequester: FocusRequester, + focusContentEvent: ComposableEvent, + pictureDataType: PictureDataType, + profile: ProfilesUseCaseData.Profile?, + shouldAnimateImage: Boolean, + isEnabled: Boolean, + imageUri: Uri, + textData: String, + isSamsungDevice: Boolean, + scrollState: ScrollState, + onTextContentReceived: (String) -> Unit, + onImageContentReceived: (Uri) -> Unit, + onColorPicked: (ProfilesData.ProfileColorNames) -> Unit, + onBack: () -> Unit, + onSelect: () -> Unit, + onEmpty: () -> Unit +) { + val context = LocalContext.current + val isDarkMode = isSystemInDarkTheme() + Scaffold( + contentColor = AppTheme.colors.neutral900, + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + NavigationTopAppBar( + navigationMode = NavigationBarMode.Close, + title = stringResource(R.string.profile_image_selector_title), + onBack = onBack + ) + }, + bottomBar = { + SaveButton( + text = stringResource(id = R.string.onboarding_bottom_button_save), + isEnabled = isEnabled, + onEmpty = onEmpty + ) { onSelect() } + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(AppTheme.colors.neutral025) + .padding(it) + .verticalScroll(scrollState) + ) { + SpacerMedium() + AnimatedVisibility(pictureDataType == PictureDataType.EMPTY) { + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(SizeDefaults.twelvefold) + .clip(CircleShape) + .clickable { focusContentEvent.trigger() } + .aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier + .animatedCircularBorder(shouldAnimateImage) + .size(SizeDefaults.tenfold) + .alpha(0.8f), + colorFilter = ColorFilter.tint(AppTheme.colors.neutral999), + imageVector = Icons.Rounded.EmojiEmotions, + contentDescription = null + ) + } + } + } + SpacerMedium() + Box( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + if (pictureDataType == PictureDataType.TEXT) { + createCircularBitmapFromText( + context = context, + text = textData, + isDarkMode = isDarkMode, + backgroundColor = profile?.color?.color()?.backGroundColor ?: Color.White + )?.let { bitmap -> + CircularBitmapImage( + modifier = Modifier + .size(SizeDefaults.twelvefold) + .clip(CircleShape) + .circularBorder(profile?.color?.color()?.borderColor ?: Color.Gray) + .background(profile?.color?.color()?.backGroundColor ?: Color.White) + .clickable { focusContentEvent.trigger() }, + image = bitmap + ) + } + } else if (pictureDataType == PictureDataType.IMAGE) { + AsyncImage( + model = imageUri, + contentDescription = null, + modifier = Modifier + .size(SizeDefaults.twelvefold) + .clip(CircleShape) + .circularBorder(profile?.color?.color()?.borderColor ?: Color.Gray) + .background(profile?.color?.color()?.backGroundColor ?: Color.White) + .clickable { focusContentEvent.trigger() }, + contentScale = ContentScale.Fit + ) + } + } + SpacerTiny() + if (isSamsungDevice) { + BasicTextField( + value = textData, + onValueChange = { value -> + Napier.i { "onValueChange: $value" } + onTextContentReceived(value) + }, + keyboardOptions = KeyboardOptions.Default, + modifier = Modifier + .alpha(0f) // hide the text field and only use the receiveContent + .height(SizeDefaults.one) + .focusRequester(focusRequester) + ) + } else { + @Suppress("LoopWithTooManyJumpStatements") + ( + BasicTextField2( + value = textData, + onValueChange = { value -> + Napier.i { "onValueChange: $value" } + onTextContentReceived(value) + }, + keyboardOptions = KeyboardOptions.Default, + modifier = Modifier + .hideFromView() + .focusRequester(focusRequester) + .receiveContent(setOf(MediaType.All)) { content -> + if (content.hasMediaType(MediaType.Image)) { + val clipData = content.clipEntry.clipData + for (index in 0 until clipData.itemCount) { + val item = clipData.getItemAt(index) ?: continue + onImageContentReceived(item.uri) + item.uri ?: continue + } + } + content + } + ) + ) + } + + profile?.let { editableProfile -> + CenterColumn { + ProfileBackgroundColorComponent( + color = editableProfile.color, + onColorPicked = onColorPicked + ) + } + } + + SpacerXXXLarge() + } + } +} + +@Suppress("MagicNumber") +private fun createCircularBitmapFromText( + context: Context, + text: String, + sizeDp: Int = SIZE_DP, + isDarkMode: Boolean, + backgroundColor: Color +): Bitmap? { + try { + val density = context.resources.displayMetrics.density + val textSizeSp = TEXT_SIZE + val backgroundPaint = Paint().apply { + color = backgroundColor.toArgb() + textSize = textSizeSp * density + isAntiAlias = true // For smooth edges + } + + val colouredPaint = Paint().apply { + color = when { + isDarkMode -> Color(0xFFE0E0E0).toArgb() + else -> Color(0xFF757575).toArgb() + } + textSize = textSizeSp * density + isAntiAlias = true // For smooth edges + } + + // Calculate the diameter of the circular bounding box + val diameter = sizeDp * density + + val bitmap = Bitmap.createBitmap(diameter.toInt(), diameter.toInt(), Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + // Calculate the horizontal position (x) to center the text // diameter / DIAMETER_X + val x = when { + containsEmoji(text) -> 95f + else -> 90f + } + + val ascent = -backgroundPaint.ascent() + val descent = backgroundPaint.descent() + val textHeight = ascent + descent + // Calculate the vertical position (y) to center the text + val y = (diameter - textHeight) / DIAMETER_Y + ascent + + val rect = RectF(0f, 0f, diameter, diameter) + + // Draw the white circle + canvas.drawOval(rect, backgroundPaint) + + // Draw the text + canvas.drawText(text, x, y, colouredPaint) + + return bitmap + } catch (e: Exception) { + // If an exception occurs, return null + Napier.e { "Error on bitmap creation ${e.stackTraceToString()}" } + return null + } +} + +private fun uriToBitmap(context: Context, uri: Uri): Bitmap? { + val inputStream = context.contentResolver.openInputStream(uri) + return inputStream?.use { + BitmapFactory.decodeStream(it) + } +} + +private fun containsEmoji(text: String): Boolean { + val emojiPattern = Regex( + "[\\x{1F600}-\\x{1F64F}|" + // Emoticons + "\\x{1F300}-\\x{1F5FF}|" + // Misc Symbols and Pictographs + "\\x{1F680}-\\x{1F6FF}|" + // Transport and Map + "\\x{1F700}-\\x{1F77F}|" + // Alchemical Symbols + "\\x{1F780}-\\x{1F7FF}|" + // Geometric Shapes Extended + "\\x{1F800}-\\x{1F8FF}|" + // Supplemental Arrows-C + "\\x{1F900}-\\x{1F9FF}|" + // Supplemental Symbols and Pictographs + "\\x{1FA70}-\\x{1FAFF}|" + // Symbols and Pictographs Extended-A + "\\x{2600}-\\x{26FF}|" + // Misc symbols (e.g., sun, moon, umbrella, etc.) + "\\x{2700}-\\x{27BF}|" + // Dingbats + "\\x{2300}-\\x{23FF}|" + // Misc Technical + "\\x{2B50}-\\x{2BFF}|" + // Additional symbols + "\\x{1F1E6}-\\x{1F1FF}]", // Flags (iOS) + RegexOption.IGNORE_CASE + ) + return emojiPattern.containsMatchIn(text) +} + +private fun Modifier.hideFromView() = alpha(0f).height(SizeDefaults.one) + +@LightDarkPreview +@Composable +internal fun ProfileImageEmojiScreenContentEmojiPreview() { + PreviewAppTheme { + val scrollState = rememberScrollState() + ProfileImageEmojiScreenContent( + snackbarHostState = SnackbarHostState(), + focusRequester = FocusRequester(), + focusContentEvent = ComposableEvent(), + pictureDataType = PictureDataType.TEXT, + profile = null, + shouldAnimateImage = false, + imageUri = Uri.EMPTY, + textData = "\uD83D\uDE00", + isSamsungDevice = false, + isEnabled = true, + scrollState = scrollState, + onTextContentReceived = { }, + onImageContentReceived = { }, + onColorPicked = { }, + onBack = { }, + onSelect = { }, + onEmpty = { } + ) + } +} + +@LightDarkPreview +@Composable +internal fun ProfileImageEmojiScreenContentTextPreview() { + PreviewAppTheme { + val scrollState = rememberScrollState() + ProfileImageEmojiScreenContent( + snackbarHostState = SnackbarHostState(), + focusRequester = FocusRequester(), + focusContentEvent = ComposableEvent(), + pictureDataType = PictureDataType.TEXT, + profile = null, + shouldAnimateImage = false, + imageUri = Uri.EMPTY, + textData = "10", + isSamsungDevice = false, + isEnabled = true, + scrollState = scrollState, + onTextContentReceived = { }, + onImageContentReceived = { }, + onColorPicked = { }, + onBack = { }, + onSelect = { }, + onEmpty = { } + ) + } +} + +@LightDarkPreview +@Composable +internal fun ProfileImageEmojiScreenContentEmptyPreview() { + PreviewAppTheme { + val scrollState = rememberScrollState() + ProfileImageEmojiScreenContent( + snackbarHostState = SnackbarHostState(), + focusRequester = FocusRequester(), + focusContentEvent = ComposableEvent(), + pictureDataType = PictureDataType.EMPTY, + profile = null, + shouldAnimateImage = true, + imageUri = Uri.EMPTY, + textData = "", + isSamsungDevice = false, + isEnabled = true, + scrollState = scrollState, + onTextContentReceived = { }, + onImageContentReceived = { }, + onColorPicked = { }, + onBack = { }, + onSelect = { }, + onEmpty = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfilePairedDevicesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfilePairedDevicesScreen.kt new file mode 100644 index 00000000..6a525cbd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfilePairedDevicesScreen.kt @@ -0,0 +1,386 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.authentication.ui.components.AuthenticationFailureDialog +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.NoInternetError +import de.gematik.ti.erp.app.profiles.model.ProfilePairedDevicesErrorState.UserNotLoggedInWithBiometricsError +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilePairedDevicesScreenController +import de.gematik.ti.erp.app.profiles.ui.components.DeleteDeviceDialog +import de.gematik.ti.erp.app.profiles.ui.preview.PairedDevicesPreviewParameterProvider +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.collectLatest + +private const val VERTICAL_BIAS_ALIGNMENT = -0.33f +private const val HORIZONTAL_BIAS_ALIGNMENT = 0f + +class ProfilePairedDevicesScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val profileId = remember { requireNotNull(navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID)) } + val intentHandler = LocalIntentHandler.current + + val deleteDeviceEvent = ComposableEvent() + + val controller = rememberProfilePairedDevicesScreenController(profileId) + + val pairedDevicesState by controller.pairedDevices.collectAsStateWithLifecycle() + + navBackStackEntry.onReturnAction(ProfileRoutes.ProfilePairedDevicesScreen) { + controller.refreshPairedDevices() + } + + with(controller) { + showCardWallEvent.listen { id -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(id)) + } + showCardWallWithFilledCanEvent.listen { cardWallData -> + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = cardWallData.profileId, + can = cardWallData.can + ) + ) + } + showGidEvent.listen { gidData -> + navController.navigate( + CardWallRoutes.CardWallIntroScreen.pathWithGid( + profileIdentifier = gidData.profileId, + gidEventData = gidData + ) + ) + } + } + + LaunchedEffect(Unit) { + intentHandler.gidSuccessfulIntent.collectLatest { + controller.refreshPairedDevices() + } + } + + AuthenticationFailureDialog( + event = controller.showAuthenticationErrorDialog, + dialogScaffold = dialog + ) + + DeleteDeviceDialog( + event = deleteDeviceEvent, + dialogScaffold = dialog, + onClickAction = { + controller.deletePairedDevice(device = it) + } + ) + + ProfilePairedDevicesScreenScaffold( + state = pairedDevicesState, + listState = listState, + onBack = { navController.popBackStack() }, + onDeleteDevice = { device -> deleteDeviceEvent.trigger(device) }, + onAuthenticate = { + controller.chooseAuthenticationMethod( + profileId = profileId, + useBiometricPairingScope = true + ) + }, + onRefresh = { controller.refreshPairedDevices() } + ) + } +} + +@Composable +private fun ProfilePairedDevicesScreenScaffold( + state: UiState>, + listState: LazyListState, + onBack: () -> Unit, + onDeleteDevice: (PairedDevice) -> Unit, + onAuthenticate: () -> Unit, + onRefresh: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.paired_devices_title), + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = onBack + ) { + UiStateMachine( + state = state, + onLoading = { + EmptyScreenLoading(modifier = Modifier.fillMaxSize()) + }, + onEmpty = { + EmptyScreenNoDevices(modifier = Modifier.fillMaxSize()) + }, + onError = { error -> + val (title, description, retryText) = (error as ProfilePairedDevicesErrorState).errorParams() + val isBiometricError = error is UserNotLoggedInWithBiometricsError + EmptyScreenFailure( + modifier = Modifier.fillMaxSize(), + isBiometricError = isBiometricError, + title = title, + description = description, + retryText = retryText, + onClickRetry = { if (isBiometricError) onAuthenticate() else onRefresh() } + ) + } + ) { pairedDevices -> + PairedDevicesSection( + listState = listState, + devices = pairedDevices, + onDeleteDevice = onDeleteDevice + ) + } + } +} + +// TODO: Move to components +@Composable +private fun PairedDevicesSection( + modifier: Modifier = Modifier, + listState: LazyListState, + devices: List, + onDeleteDevice: (PairedDevice) -> Unit +) { + LazyColumn( + state = listState, + modifier = modifier + ) { + items(items = devices) { device -> + PairedDevice( + device = device, + isOurDevice = device.isCurrentDevice, + onDeleteDevice = { + onDeleteDevice(device) + } + ) + } + } +} + +@Composable +private fun EmptyScreenLoading(modifier: Modifier) { + EmptyScreen(modifier) { + CircularProgressIndicator(Modifier.size(SizeDefaults.sixfold)) + Text( + stringResource(R.string.paired_devices_loading_description), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun EmptyScreenNoDevices(modifier: Modifier = Modifier) { + EmptyScreen(modifier) { + Text( + stringResource(R.string.paired_devices_no_devices_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + SpacerSmall() + Text( + stringResource(R.string.paired_devices_no_devices_description), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun EmptyScreenFailure( + modifier: Modifier, + isBiometricError: Boolean, + title: String, + description: String, + retryText: String = stringResource(R.string.paired_devices_error_retry), + onClickRetry: () -> Unit +) { + EmptyScreen(modifier) { + if (isBiometricError) { + Image( + painterResource(R.drawable.card_wall_card_hand), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .size(SizeDefaults.twentyfivefold) + .fillMaxWidth() + .wrapContentHeight() + ) + SpacerMedium() + } + Text( + title, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + Text( + description, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + TextButton(onClick = onClickRetry) { + Icon(Icons.Rounded.Refresh, null) + SpacerSmall() + Text(retryText) + } + } +} + +@Suppress("MagicNumber") +@Composable +internal fun EmptyScreen( + modifier: Modifier, + content: @Composable () -> Unit +) { + Box(modifier) { + Column( + modifier = Modifier + .align(BiasAlignment(HORIZONTAL_BIAS_ALIGNMENT, VERTICAL_BIAS_ALIGNMENT)) + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + content() + } + } +} + +@Composable +private fun PairedDevice( + device: PairedDevice, + isOurDevice: Boolean, + onDeleteDevice: () -> Unit +) { + Row(Modifier.padding(PaddingDefaults.Medium)) { + Column(Modifier.weight(1f)) { + Text(device.name, style = AppTheme.typography.body1) + // val connectedOn = localizedDateString(device.connectedOn) + if (isOurDevice) { + Text( + stringResource(R.string.paired_device_subtitle_our_device, device.connectedOn), + style = AppTheme.typography.body2l + ) + } else { + Text(stringResource(R.string.paired_device_subtitle, device.connectedOn), style = AppTheme.typography.body2l) + } + } + SpacerMedium() + IconButton(onClick = onDeleteDevice) { + Icon(Icons.Rounded.Delete, null, tint = AppTheme.colors.neutral500) + } + } +} + +@Composable +private fun ProfilePairedDevicesErrorState.errorParams(): Triple { + return when (this) { + is NoInternetError -> Triple( + stringResource(R.string.paired_devices_error_no_network_title), + stringResource(R.string.paired_devices_error_no_network_description), + stringResource(R.string.paired_devices_error_retry) + ) + + is UserNotLoggedInWithBiometricsError -> Triple( + stringResource(R.string.paired_devices_biometrics_error_login_title), + stringResource(R.string.paired_devices_biometrics_error_login_description), + stringResource(R.string.paired_devices_biometrics_error_login_button_text) + ) + + else -> Triple( + stringResource(R.string.paired_devices_error_generic_title), + stringResource(R.string.paired_devices_error_generic_description), + stringResource(R.string.paired_devices_error_retry) + ) + } +} + +@LightDarkPreview +@Composable +fun ProfilePairedDevicesScreenScaffoldPreview( + @PreviewParameter(PairedDevicesPreviewParameterProvider::class) state: UiState> +) { + PreviewAppTheme { + ProfilePairedDevicesScreenScaffold( + state = state, + listState = rememberLazyListState(), + onBack = {}, + onDeleteDevice = {}, + onAuthenticate = {}, + onRefresh = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileScreen.kt new file mode 100644 index 00000000..ba48e241 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/screens/ProfileScreen.kt @@ -0,0 +1,909 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CloudQueue +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.rounded.AddAPhoto +import androidx.compose.material.icons.rounded.Devices +import androidx.compose.material.icons.rounded.EuroSymbol +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pkv.navigation.PkvRoutes +import de.gematik.ti.erp.app.profiles.model.ProfileCombinedData +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.presentation.rememberProfileScreenController +import de.gematik.ti.erp.app.profiles.ui.components.ChooseAvatar +import de.gematik.ti.erp.app.profiles.ui.components.color +import de.gematik.ti.erp.app.profiles.ui.components.profileColor +import de.gematik.ti.erp.app.profiles.ui.preview.ProfileStatePreviewParameterProvider +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.DynamicText +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.ErrorText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.circularBorder +import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant + +class ProfileScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + val keyboardController = LocalSoftwareKeyboardController.current + val dialog = LocalDialog.current + + val deleteDialogEvent = ComposableEvent() + + val profileId = + remember { navBackStackEntry.arguments?.getString(ProfileRoutes.PROFILE_NAV_PROFILE_ID) } ?: return + val profileScreenController = rememberProfileScreenController(profileId) + val combinedProfileState by profileScreenController.combinedProfile.collectAsStateWithLifecycle() + + @Requirement( + "A_19229-01#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Deletion button is tapped -> delete confirmation dialog shows." + ) + val defaultProfileName = stringResource(id = R.string.onboarding_default_profile_name) + DeleteProfileDialog( + event = deleteDialogEvent, + dialogScaffold = dialog, + onClickAction = { + profileScreenController.deleteProfile(profileId, defaultProfileName) + navController.popBackStack() + } + ) + + ProfileScreenScaffold( + profileState = combinedProfileState, + color = color, + keyboardController = keyboardController, + onClickLogIn = { + combinedProfileState.data?.selectedProfile?.let { profile -> + profileScreenController.switchActiveProfile(profile.id) + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(profile.id)) + } + }, + onClickLogout = { + combinedProfileState.data?.selectedProfile?.let { profile -> + profileScreenController.logout(profile.id) + profileScreenController.refreshCombinedProfile() + } + }, + onClickDelete = { + deleteDialogEvent.trigger(Unit) + }, + onUpdateProfileName = { name -> + profileScreenController.updateProfileName(name) + }, + onClickEditAvatar = { + combinedProfileState.data?.selectedProfile?.let { profile -> + navController.navigate(ProfileRoutes.ProfileEditPictureScreen.path(profileId = profile.id)) + } + }, + onClickInvoices = { + combinedProfileState.data?.selectedProfile?.let { profile -> + navController.navigate(PkvRoutes.InvoiceListScreen.path(profileId = profile.id)) + } + }, + onShowPairedDevices = { + combinedProfileState.data?.selectedProfile?.let { profile -> + navController.navigate(ProfileRoutes.ProfilePairedDevicesScreen.path(profileId = profile.id)) + } + }, + onClickAuditEvents = { + combinedProfileState.data?.selectedProfile?.let { profile -> + navController.navigate(ProfileRoutes.ProfileAuditEventsScreen.path(profileId = profile.id)) + } + } + ) { navController.popBackStack() } + } +} + +@Suppress("LongParameterList") +@Composable +internal fun ProfileScreenScaffold( + profileState: UiState, + color: Color, + keyboardController: SoftwareKeyboardController?, + onClickLogIn: () -> Unit, + onClickLogout: () -> Unit, + onClickDelete: () -> Unit, + onUpdateProfileName: (String) -> Unit, + onClickEditAvatar: () -> Unit, + onClickInvoices: () -> Unit, + onShowPairedDevices: () -> Unit, + onClickAuditEvents: () -> Unit, + onBack: () -> Unit +) { + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + + AnimatedElevationScaffold( + modifier = + Modifier.imePadding(), + topBarTitle = stringResource(R.string.edit_profile_title), + navigationMode = NavigationBarMode.Back, + scaffoldState = scaffoldState, + listState = listState, + actions = { + profileState.data?.selectedProfile?.let { + ThreeDotMenu( + selectedProfile = it, + onClickLogIn = onClickLogIn, + onClickLogout = onClickLogout, + onClickDelete = onClickDelete + ) + } + }, + onBack = onBack + ) { + ProfileScreenContent( + listState = listState, + profileState = profileState, + keyboardController = keyboardController, + color = color, + onUpdateProfileName = onUpdateProfileName, + onClickEditAvatar = onClickEditAvatar, + onClickLogIn = onClickLogIn, + onClickInvoices = onClickInvoices, + onShowPairedDevices = onShowPairedDevices, + onClickAuditEvents = onClickAuditEvents + ) + } +} + +@Suppress("LongParameterList") +@Composable +internal fun ProfileScreenContent( + listState: LazyListState, + profileState: UiState, + color: Color, + keyboardController: SoftwareKeyboardController?, + onUpdateProfileName: (String) -> Unit, + onClickEditAvatar: () -> Unit, + onClickLogIn: () -> Unit, + onClickInvoices: () -> Unit, + onShowPairedDevices: () -> Unit, + onClickAuditEvents: () -> Unit +) { + val activity = LocalActivity.current as? BaseActivity + val isDemoMode = remember { activity?.isDemoMode?.value ?: false } + UiStateMachine( + state = profileState, + onError = { + ErrorScreenComponent() + }, + onEmpty = { + ErrorScreenComponent() + }, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onContent = { profileCombinedData -> + profileCombinedData.selectedProfile?.let { selectedProfile -> + LazyColumn( + modifier = Modifier.testTag(TestTag.Profile.ProfileScreenContent), + state = listState, + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { + item { + ProfileNameSection(profileState, color, keyboardController) { name -> + onUpdateProfileName(name) + } + } + item { + SpacerLarge() + ProfileAvatarSection(profile = selectedProfile) { + onClickEditAvatar() + } + } + item { + ProfileInsuranceInformation( + selectedProfile.lastAuthenticated, + selectedProfile.ssoTokenScope, + selectedProfile.insurance + ) { + onClickLogIn() + } + } + + if (selectedProfile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { + item { + ProfileInvoiceInformation { + onClickInvoices() + } + } + } + + if (!isDemoMode) { + item { + ProfileEditPairedDeviceSection { + onShowPairedDevices() + } + } + } + + item { + SecuritySection { + onClickAuditEvents() + } + } + } + } + } + ) +} + +@Composable +private fun ProfileEditPairedDeviceSection(onShowPairedDevices: () -> Unit) { + Text( + text = stringResource(R.string.settings_paired_devices_title), + style = AppTheme.typography.h6, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, + modifier = + Modifier + .fillMaxWidth() + .clickable( + onClick = onShowPairedDevices + ) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Icon(Icons.Rounded.Devices, null, tint = AppTheme.colors.primary600) + Text(stringResource(R.string.settings_login_connected_devices), style = AppTheme.typography.body1) + } + SpacerLarge() + Divider(modifier = Modifier.padding(horizontal = PaddingDefaults.Medium)) + SpacerLarge() +} + +@Composable +private fun ProfileInvoiceInformation(onClick: () -> Unit) { + Column { + Text( + stringResource( + id = R.string.profile_invoiceInformation_header + ), + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.h6 + ) + SpacerSmall() + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, + modifier = + Modifier + .fillMaxWidth() + .clickable( + onClick = onClick + ) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Icon(Icons.Rounded.EuroSymbol, null, tint = AppTheme.colors.primary600) + Text( + stringResource( + R.string.profile_show_invoices + ), + style = AppTheme.typography.body1 + ) + } + SpacerLarge() + Divider() + SpacerLarge() + } +} + +@Composable +internal fun ThreeDotMenu( + selectedProfile: ProfilesUseCaseData.Profile, + onClickLogIn: () -> Unit, + onClickLogout: () -> Unit, + onClickDelete: () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true }, + modifier = Modifier.testTag(TestTag.Profile.ThreeDotMenuButton) + ) { + Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = DpOffset(24.dp, 0.dp) + ) { + DropdownMenuItem( + modifier = + Modifier.testTag( + if (selectedProfile.ssoTokenScope != null) { + TestTag.Profile.LogoutButton + } else { + TestTag.Profile.LoginButton + } + ), + onClick = + if (selectedProfile.ssoTokenScope != null) { + onClickLogout + } else { + onClickLogIn + } + ) { + Text( + text = + if (selectedProfile.ssoTokenScope != null) { + stringResource(R.string.insurance_information_logout) + } else { + stringResource(R.string.insurance_information_login) + } + ) + } + + DropdownMenuItem( + modifier = Modifier.testTag(TestTag.Profile.DeleteProfileButton), + onClick = { + expanded = false + onClickDelete() + } + ) { + Text( + text = stringResource(R.string.remove_profile), + color = AppTheme.colors.red600 + ) + } + } +} + +@Composable +internal fun DeleteProfileDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onClickAction: () -> Unit +) { + event.listen { + dialogScaffold.show { dialog -> + ErezeptAlertDialog( + title = stringResource(id = R.string.remove_profile_header), + bodyText = stringResource(R.string.remove_profile_detail_message), + confirmText = stringResource(R.string.remove_profile_yes), + dismissText = stringResource(R.string.remove_profile_no), + onConfirmRequest = { + onClickAction() + dialog.dismiss() + }, + onDismissRequest = { dialog.dismiss() } + ) + } + } +} + +@Requirement( + "O.Auth_6#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Button to display audit events for profile." +) +@Composable +private fun SecuritySection(onClickAuditEvents: () -> Unit) { + Text( + text = stringResource(R.string.settings_app_security_header), + style = AppTheme.typography.h6, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) + SecurityAuditEventsSubSection(onClickAuditEvents) +} + +@Requirement( + "A_19177#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Button to display audit events for profile." +) +@Composable +private fun SecurityAuditEventsSubSection(onClickAuditEvents: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, + modifier = + Modifier + .fillMaxWidth() + .clickable( + onClick = { + onClickAuditEvents() + } + ) + .testTag(TestTag.Profile.OpenAuditEventsScreenButton) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Icon(Icons.Outlined.CloudQueue, null, tint = AppTheme.colors.primary600) + Column { + Text( + stringResource( + R.string.settings_show_audit_events + ), + style = AppTheme.typography.body1 + ) + Text( + stringResource( + R.string.settings_show_audit_events_info + ), + style = AppTheme.typography.body2l + ) + } + } +} + +@Composable +private fun ProfileNameSection( + profileState: UiState, + color: Color, + keyboardController: SoftwareKeyboardController?, + onUpdateProfileName: (String) -> Unit +) { + val initialProfileName = profileState.data?.selectedProfile?.name ?: "" + var profileName by remember(profileState) { mutableStateOf(initialProfileName) } + var profileNameValid by remember { mutableStateOf(true) } + var textFieldEnabled by remember { mutableStateOf(false) } + + val focusRequester = remember { FocusRequester() } + val scope = rememberCoroutineScope() + + LaunchedEffect(textFieldEnabled) { + if (textFieldEnabled) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + + Column { + Row( + modifier = Modifier.padding(PaddingDefaults.Medium) + ) { + if (!textFieldEnabled) { + val txt = + buildAnnotatedString { + append(profileName) + append(" ") + appendInlineContent("edit", "edit") + } + val inlineContent = + mapOf( + "edit" to + InlineTextContent( + Placeholder( + width = 0.em, + height = 0.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ) + ) { + Icon(Icons.Outlined.Edit, null, tint = AppTheme.colors.neutral400) + } + ) + DynamicText( + txt, + style = AppTheme.typography.h5, + inlineContent = inlineContent, + modifier = + Modifier + .clickable { + textFieldEnabled = true + } + .testTag(TestTag.Profile.EditProfileNameButton) + ) + } else { + ProfileEditBasicTextField( + modifier = + Modifier + .weight(1f) + .focusRequester(focusRequester) + .testTag(TestTag.Profile.NewProfileNameField), + enabled = textFieldEnabled, + initialProfileName = initialProfileName, + onChangeProfileName = { name: String, isValid: Boolean -> + profileName = name + profileNameValid = isValid + }, + color = color, + profiles = profileState.data?.profiles ?: emptyList(), + onDone = { + if (profileNameValid) { + onUpdateProfileName(profileName) + textFieldEnabled = false + scope.launch { keyboardController?.hide() } + } + } + ) + } + } + + if (!profileNameValid) { + SpacerTiny() + val errorText = + if (profileName.isBlank()) { + stringResource(R.string.edit_profile_empty_profile_name) + } else { + stringResource(R.string.edit_profile_duplicated_profile_name) + } + + ErrorText( + text = errorText, + modifier = Modifier.padding(start = PaddingDefaults.Medium) + ) + } + } +} + +@Composable +private fun ProfileEditBasicTextField( + modifier: Modifier, + enabled: Boolean, + textStyle: TextStyle = AppTheme.typography.h5, + color: Color, + initialProfileName: String, + onChangeProfileName: (String, Boolean) -> Unit, + profiles: List, + onDone: () -> Unit +) { + var profileNameState by remember { + mutableStateOf( + TextFieldValue( + text = initialProfileName, + selection = TextRange(initialProfileName.length) + ) + ) + } + + val mergedTextStyle = textStyle.merge(TextStyle(color = color)) + + BasicTextField( + value = profileNameState, + onValueChange = { + val name = it.text.trimStart().sanitizeProfileName() + profileNameState = + TextFieldValue( + text = name, + selection = it.selection, + composition = it.composition + ) + + onChangeProfileName( + name, + name.trim().equals(initialProfileName, true) || + !profiles.containsProfileWithName(name) && + name.isNotEmpty() + ) + }, + enabled = enabled, + singleLine = !enabled, + textStyle = mergedTextStyle, + modifier = modifier, + cursorBrush = SolidColor(color), + keyboardOptions = + KeyboardOptions( + autoCorrect = false, + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { onDone() }) + ) +} + +@Composable +private fun ProfileAvatarSection( + profile: ProfilesUseCaseData.Profile, + onClickEditAvatar: () -> Unit +) { + val selectedColor = profileColor(profileColorNames = profile.color) + + Column( + modifier = + Modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium) + ) { + SpacerTiny() + Surface( + modifier = + Modifier + .size(140.dp) + .align(Alignment.CenterHorizontally), + shape = CircleShape, + color = selectedColor.backGroundColor + ) { + Box( + modifier = + Modifier + .fillMaxSize() + .circularBorder(selectedColor.borderColor) + .clickable(onClick = onClickEditAvatar), + contentAlignment = Alignment.Center + ) { + ChooseAvatar( + emptyIcon = Icons.Rounded.AddAPhoto, + modifier = Modifier.size(SizeDefaults.triple), + image = profile.image, + profileColor = profile.color.color(), + avatar = profile.avatar + ) + } + } + SpacerSmall() + TextButton( + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .testTag(TestTag.Profile.EditProfileImageButton), + onClick = onClickEditAvatar + ) { + Text(text = stringResource(R.string.edit_profile_avatar), textAlign = TextAlign.Center) + } + } +} + +@Composable +private fun ProfileInsuranceInformation( + lastAuthenticated: Instant?, + ssoTokenScope: IdpData.SingleSignOnTokenScope?, + insuranceInformation: ProfileInsuranceInformation, + onClickLogIn: () -> Unit +) { + SpacerLarge() + val cardAccessNumber = + if (ssoTokenScope is IdpData.TokenWithHealthCardScope) { + ssoTokenScope.cardAccessNumber + } else { + null + } + + Column { + Text( + stringResource( + id = R.string.insurance_information_header + ), + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.h6 + ) + SpacerSmall() + + if (lastAuthenticated != null) { + LabeledText( + stringResource(R.string.insurance_information_insurant_name), + insuranceInformation.insurantName + ) + LabeledText( + stringResource(R.string.insurance_information_insurance_name), + insuranceInformation.insuranceName + ) + cardAccessNumber?.let { + LabeledText(stringResource(R.string.insurance_information_insurant_can), it) + } + LabeledText( + stringResource(R.string.insurance_information_insurance_identifier), + insuranceInformation.insuranceIdentifier, + Modifier.testTag(TestTag.Profile.InsuranceId) + ) + } + + if (ssoTokenScope != null) { + LabeledText( + stringResource(R.string.profile_insurance_information_connected_label), + when (ssoTokenScope) { + is IdpData.DefaultToken -> + stringResource( + R.string.profile_insurance_information_connected_health_card + ) + + is IdpData.ExternalAuthenticationToken -> ssoTokenScope.authenticatorName + is IdpData.AlternateAuthenticationToken, + is IdpData.AlternateAuthenticationWithoutToken + -> + stringResource( + R.string.profile_insurance_information_connected_biometrics + ) + } + ) + } else { + ClickableLabeledTextWithIcon( + description = stringResource(R.string.profile_insurance_information_connected_label), + content = stringResource(R.string.profile_insurance_information_not_connected), + icon = Icons.Rounded.Refresh + ) { + onClickLogIn() + } + } + SpacerLarge() + Divider() + SpacerLarge() + } +} + +/** + * Shows the given content if != null labeled with a description as described in design guide for ProfileScreen. + */ +@Composable +private fun LabeledText( + description: String, + content: String, + modifier: Modifier = Modifier +) { + Column(modifier.padding(PaddingDefaults.Medium)) { + Text(content, style = AppTheme.typography.body1) + Text(description, style = AppTheme.typography.body2l) + } +} + +@Composable +private fun ClickableLabeledTextWithIcon( + description: String, + content: String, + modifier: Modifier = Modifier, + icon: ImageVector, + onClick: () -> Unit +) { + Row( + modifier = + modifier + .fillMaxWidth() + .clickable { + onClick() + } + .padding(PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text(content, style = AppTheme.typography.body1) + Text(description, style = AppTheme.typography.body2l) + } + Icon(icon, contentDescription = null, tint = AppTheme.colors.primary600) + } +} + +@LightDarkPreview +@Composable +fun ProfileScreenPreview( + @PreviewParameter(ProfileStatePreviewParameterProvider::class) profileState: UiState +) { + val listState = rememberLazyListState() + PreviewAppTheme { + val color = AppTheme.colors.primary600 + ProfileScreenContent( + listState = listState, + profileState = profileState, + color = color, + keyboardController = null, + onUpdateProfileName = {}, + onClickEditAvatar = {}, + onClickLogIn = {}, + onClickInvoices = {}, + onShowPairedDevices = {}, + onClickAuditEvents = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt index 06ad2a37..de5f66b5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase @@ -32,7 +32,7 @@ class AddProfileUseCase( ) { suspend operator fun invoke(name: String) { withContext(dispatcher) { - repository.saveProfile(name, activate = true) + repository.createNewProfile(name) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt index df748f77..02fd3cd6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeletePairedDevicesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeletePairedDevicesUseCase.kt new file mode 100644 index 00000000..6634f1d4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeletePairedDevicesUseCase.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.usecase.IdpUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class DeletePairedDevicesUseCase( + private val idpUseCase: IdpUseCase, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + suspend operator fun invoke( + profileId: ProfileIdentifier, + device: PairedDevice + ): Result = + withContext(dispatcher) { + idpUseCase.deletePairedDevice(profileId = profileId, deviceAlias = device.alias).map { + device.name + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt index cd3d699c..9a414bb0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.repository.ProfileRepository -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -33,10 +33,10 @@ class DeleteProfileUseCase( private val idpRepository: IdpRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - suspend operator fun invoke(profile: Profile) { + suspend operator fun invoke(profileIdentifier: ProfileIdentifier, profileName: String) { withContext(dispatcher) { - idpRepository.invalidateDecryptedAccessToken(profile.id) - profileRepository.removeProfile(profile.id) + idpRepository.invalidateDecryptedAccessToken(profileIdentifier) + profileRepository.removeProfile(profileIdentifier, profileName = profileName) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt index 30be8b08..74222086 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetPairedDevicesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetPairedDevicesUseCase.kt new file mode 100644 index 00000000..ac71d8b3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetPairedDevicesUseCase.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.usecase.IdpUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +class GetPairedDevicesUseCase( + private val idpUseCase: IdpUseCase, + formatStyle: FormatStyle = FormatStyle.LONG, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke( + profileId: ProfileIdentifier, + keyStoreAlias: String + ): Flow> = + flow { + emit( + idpUseCase.getPairedDevices(profileId).getOrThrow() + .map { (pairingResponseEntry, pairingData) -> + val creationTime = Instant.fromEpochSeconds(pairingResponseEntry.creationTime) + PairedDevice( + name = pairingResponseEntry.name, + alias = pairingData.keyAliasOfSecureElement, + connectedOn = creationTime + .toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime().format(dateTimeFormatter), + isCurrentDevice = keyStoreAlias == pairingData.keyAliasOfSecureElement + ) + }.sortedByDescending(PairedDevice::connectedOn) + ) + }.flowOn(dispatcher) + + private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(formatStyle) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfileByIdUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfileByIdUseCase.kt new file mode 100644 index 00000000..7308a6ce --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfileByIdUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class GetProfileByIdUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(id: ProfileIdentifier): Flow = + repository.getProfileById(id).map { it.toModel() }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt index 454e2225..f3334763 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt index 9262f2dd..e29ce2ea 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/IsProfilePKVUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/IsProfilePKVUseCase.kt new file mode 100644 index 00000000..e626dc40 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/IsProfilePKVUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class IsProfilePKVUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(id: ProfileIdentifier): Boolean { + return withContext(dispatcher) { + repository.checkIsProfilePKV(id) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt index bbe9e4a0..14ff1db6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt index 829b75ed..9e4ff191 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase -import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.model.ProfilesData @@ -33,7 +32,7 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach fun List.activeProfile() = - find { profile -> profile.active }!! + find { profile -> profile.isActive }!! // TODO: Used only in test and debug-viewmodel. Remove it from there too. class ProfilesUseCase( @@ -57,10 +56,10 @@ class ProfilesUseCase( ProfilesData.InsuranceType.PKV -> ProfilesUseCaseData.InsuranceType.PKV } ), - active = profile.active, + isActive = profile.active, color = profile.color, avatar = profile.avatar, - image = profile.personalizedImage, + image = profile.image, lastAuthenticated = profile.lastAuthenticated, ssoTokenScope = profile.singleSignOnTokenScope ) @@ -89,49 +88,12 @@ class ProfilesUseCase( } ?: error("invalid profile name `$newProfileName`") } - /** - * Removes the [profile] and adds a new profile with the name set to [newProfileName]. - */ - suspend fun removeAndSaveProfile(profile: ProfilesUseCaseData.Profile, newProfileName: String) { - addProfile(newProfileName, activate = true) - - idpRepository.invalidateDecryptedAccessToken(profile.name) - profilesRepository.removeProfile(profile.id) - } - - /** - * Removes the [profile]. - */ - suspend fun removeProfile(profile: ProfilesUseCaseData.Profile) { - idpRepository.invalidateDecryptedAccessToken(profile.name) - profilesRepository.removeProfile(profile.id) - } - - @Requirement( - "O.Tokn_6#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate config and token" - ) - suspend fun logout(profile: ProfilesUseCaseData.Profile) { - idpRepository.invalidate(profile.id) - } - suspend fun updateProfileName(profileId: ProfileIdentifier, newProfileName: String) { sanitizedProfileName(newProfileName)?.also { profileName -> profilesRepository.updateProfileName(profileId, profileName) } ?: error("invalid profile name `$newProfileName`") } - suspend fun updateProfileColor(profile: ProfilesUseCaseData.Profile, color: ProfilesData.ProfileColorNames) { - profilesRepository.updateProfileColor(profile.id, color) - } - - // tag::SwitchActiveProfileUseCase[] - suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) { - profilesRepository.activateProfile(profile.id) - } - // end::SwitchActiveProfileUseCase[] - fun activeProfileId() = activeProfile().mapNotNull { it!!.id } fun activeProfile() = profilesRepository.profiles().map { @@ -139,10 +101,6 @@ class ProfilesUseCase( profile.active } } - - suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { - profilesRepository.switchProfileToPKV(profileId) - } } fun sanitizedProfileName(profileName: String): String? = diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt deleted file mode 100644 index 1875c3f7..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.usecase - -import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.datetime.Instant - -// TODO: Use the IdpUseCase and do this in the controller where it is called or do this in the IdpUseCase -class ProfilesWithPairedDevicesUseCase( - private val idpUseCase: IdpUseCase, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - fun pairedDevices(profileId: ProfileIdentifier): Flow = - flow { - emit( - ProfilesUseCaseData.PairedDevices( - idpUseCase.getPairedDevices(profileId).getOrThrow().map { (raw, pairingData) -> - PairedDevice( - name = raw.name, - alias = pairingData.keyAliasOfSecureElement, - connectedOn = Instant.fromEpochSeconds(raw.creationTime) - ) - }.sortedByDescending { - it.connectedOn - } - ) - ) - }.flowOn(dispatcher) - - suspend fun deletePairedDevices( - profileId: ProfileIdentifier, - device: PairedDevice - ): Result = - idpUseCase.deletePairedDevice(profileId = profileId, deviceAlias = device.alias).map { - device.name - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt deleted file mode 100644 index bc5ad63c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.usecase - -import de.gematik.ti.erp.app.idp.repository.IdpRepository -import de.gematik.ti.erp.app.profiles.repository.ProfileRepository -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/** - * Removes the profile from the [profileRepository] and - * invalidates it in the [idpRepository] - * adds a new profile with the new name to the [profileRepository]. - */ -class ResetProfileUseCase( - private val profileRepository: ProfileRepository, - private val idpRepository: IdpRepository, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO -) { - suspend operator fun invoke(profile: Profile, newProfileName: String) { - withContext(dispatcher) { - // Not sure if the order needs to be changed here. - profileRepository.saveProfile(newProfileName, activate = true) - idpRepository.invalidateDecryptedAccessToken(profile.name) - profileRepository.removeProfile(profile.id) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt index 894403aa..df40a693 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToGKVUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToGKVUseCase.kt new file mode 100644 index 00000000..92f569ef --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToGKVUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SwitchProfileToGKVUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(id: ProfileIdentifier): Boolean = + withContext(dispatcher) { + repository.switchProfileToGKV(id) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt index a8cca15e..ca85ce6a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase @@ -28,9 +28,8 @@ class SwitchProfileToPKVUseCase( private val repository: ProfileRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - suspend operator fun invoke(id: ProfileIdentifier) { + suspend operator fun invoke(id: ProfileIdentifier) = withContext(dispatcher) { repository.switchProfileToPKV(id) } - } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt index bea71f5d..7d548fa1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt deleted file mode 100644 index bda287d0..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.usecase.mapper - -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData - -fun ProfilesData.Profile.toModel() = - ProfilesUseCaseData.Profile( - id = id, - name = name, - insurance = ProfileInsuranceInformation( - insurantName = insurantName ?: "", - insuranceIdentifier = insuranceIdentifier ?: "", - insuranceName = insuranceName ?: "", - insuranceType = when (insuranceType) { - ProfilesData.InsuranceType.None -> ProfilesUseCaseData.InsuranceType.NONE - ProfilesData.InsuranceType.GKV -> ProfilesUseCaseData.InsuranceType.GKV - ProfilesData.InsuranceType.PKV -> ProfilesUseCaseData.InsuranceType.PKV - } - ), - active = active, - color = color, - avatar = avatar, - image = personalizedImage, - lastAuthenticated = lastAuthenticated, - ssoTokenScope = singleSignOnTokenScope - ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt index e9bdb28f..a10cfa8b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt @@ -1,28 +1,49 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.redeem -import de.gematik.ti.erp.app.redeem.usecase.RedeemUseCase +import de.gematik.ti.erp.app.redeem.presentation.DefaultOnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.usecase.GetDMCodesForLocalRedeemUseCase +import de.gematik.ti.erp.app.redeem.usecase.GetRedeemableTasksForDmCodesUseCase +import de.gematik.ti.erp.app.redeem.usecase.HasRedeemableTasksUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnDirectUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnLoggedInUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemScannedTasksUseCase import org.kodein.di.DI import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton import org.kodein.di.instance val redeemModule = DI.Module("redeemModule") { - bindProvider { RedeemUseCase(instance(), instance()) } + bindProvider { HasRedeemableTasksUseCase(instance()) } + bindProvider { GetDMCodesForLocalRedeemUseCase() } + bindProvider { RedeemScannedTasksUseCase(instance()) } + bindProvider { GetRedeemableTasksForDmCodesUseCase(instance()) } + bindProvider { RedeemPrescriptionsOnLoggedInUseCase(instance(), instance()) } + bindProvider { RedeemPrescriptionsOnDirectUseCase(instance(), instance()) } + bindSingleton { + DefaultOnlineRedeemGraphController( + instance(), + instance(), + instance(), + instance() + ) + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/CommunicationDispenseRequestModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/CommunicationDispenseRequestModels.kt new file mode 100644 index 00000000..e9db882f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/CommunicationDispenseRequestModels.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.model +import kotlinx.serialization.Serializable + +internal const val COMMUNICATION_PROFILE_1_2 = "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_DispReq|1.2" +internal const val ORDER_ID_IDENTIFIER = "https://gematik.de/fhir/NamingSystem/OrderID" +internal const val RECIPIENT_IDENTIFIER = "https://gematik.de/fhir/sid/telematik-id" + +/** + * The serialized object that is sent as a json to the Fachdienst for the pharmacy to process that an order was placed + * for a prescription/s and it needs to be processed + */ +@Serializable +internal data class Communication( + val resourceType: String = "Communication", + val meta: Meta, + val identifier: List, + val status: String = "unknown", + val basedOn: List, + val recipient: List, + val payload: List +) + +@Serializable +internal data class Meta( + val profile: List +) + +@Serializable +internal data class Identifier( + val system: String, + val value: String +) + +@Serializable +internal data class Reference( + val reference: String +) + +@Serializable +internal data class Recipient( + val identifier: RecipientIdentifier +) + +@Serializable +internal data class RecipientIdentifier( + val system: String, + val value: String +) + +@Serializable +internal data class Payload( + val contentString: String +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/DMCode.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/DMCode.kt new file mode 100644 index 00000000..8afaba96 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/DMCode.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class DMCode( + val payload: String, + val nrOfCodes: Int, + val name: String?, + val containsScanned: Boolean, + val selfPayerPrescriptionNames: List +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemDialogParameters.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemDialogParameters.kt new file mode 100644 index 00000000..c4111689 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemDialogParameters.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.model + +import androidx.annotation.StringRes + +data class RedeemDialogParameters( + @StringRes val title: Int, + @StringRes val description: Int +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemPrescriptionDialogMessageState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemPrescriptionDialogMessageState.kt new file mode 100644 index 00000000..5471c582 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemPrescriptionDialogMessageState.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.model + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.features.R + +@Requirement( + "A_20085#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Error messages on redeem prescriptions are localized" +) +@Requirement( + "O.Plat_4#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "String resources are used tp show the mapped errors." +) +sealed class RedeemPrescriptionDialogMessageState( + open val redeemDialogParameters: RedeemDialogParameters +) { + data class Success( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_200_title, + description = R.string.server_return_code_200 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class IncorrectDataStructure( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_400_title, + description = R.string.server_return_code_400 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class JsonViolated( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_title_failure, + description = R.string.server_return_code_401 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class UnableToRedeem( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_title_failure, + description = R.string.server_return_code_404 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class Timeout( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_408_title, + description = R.string.server_return_code_408 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class Conflict( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_409_title, + description = R.string.server_return_code_409 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class Gone( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_410_title, + description = R.string.server_return_code_410 + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class NotFound( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_no_code_title, + description = R.string.server_return_no_code + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class Unknown( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_no_code_title, + description = R.string.server_return_no_code + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + data class MultiplePrescriptionsFailed( + override val redeemDialogParameters: RedeemDialogParameters = RedeemDialogParameters( + title = R.string.server_return_code_title_failure, + description = R.string.several_return_code + ) + ) : RedeemPrescriptionDialogMessageState(redeemDialogParameters) + + companion object { + fun RedeemedPrescriptionState.toDialogMessageState(): RedeemPrescriptionDialogMessageState { + return when (this) { + is RedeemedPrescriptionState.Success -> Success() + is RedeemedPrescriptionState.Error -> errorState.toDialogMessageState() + else -> Unknown() + } + } + + private fun HttpErrorState.toDialogMessageState(): RedeemPrescriptionDialogMessageState = + when (this) { + HttpErrorState.BadRequest -> IncorrectDataStructure() + HttpErrorState.Conflict -> Conflict() + HttpErrorState.Gone -> Gone() + HttpErrorState.Unauthorized -> JsonViolated() + HttpErrorState.RequestTimeout -> Timeout() + HttpErrorState.NotFound -> NotFound() + // TODO: Add more ui states + HttpErrorState.ServerError, HttpErrorState.TooManyRequest, HttpErrorState.MethodNotAllowed, HttpErrorState.Forbidden, + is HttpErrorState.ErrorWithCause, HttpErrorState.Unknown -> Unknown() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemGraph.kt new file mode 100644 index 00000000..9f0fa134 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemGraph.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.redeem.ui.screens.PrescriptionSelectionScreen +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.ui.screens.HowToRedeemScreen +import de.gematik.ti.erp.app.redeem.ui.screens.LocalRedeemScreen +import de.gematik.ti.erp.app.redeem.ui.screens.OnlineRedeemPreferencesScreen +import de.gematik.ti.erp.app.redeem.ui.screens.RedeemEditShippingContactScreen +import de.gematik.ti.erp.app.redeem.ui.screens.RedeemOrderOverviewScreen +import org.kodein.di.DI +import org.kodein.di.instance + +@Suppress("LongMethod") +fun NavGraphBuilder.redeemGraph( + dependencyInjector: DI, + startDestination: String = RedeemRoutes.RedeemMethodSelection.route, + navController: NavController +) { + val onlineRedeemController by dependencyInjector.instance() + + navigation( + startDestination = startDestination, + route = RedeemRoutes.subGraphName() + ) { + renderComposable( + route = RedeemRoutes.RedeemMethodSelection.route, + arguments = RedeemRoutes.RedeemMethodSelection.arguments + ) { navEntry -> + HowToRedeemScreen( + navController = navController, + navBackStackEntry = navEntry, + controller = onlineRedeemController + ) + } + + renderComposable( + route = RedeemRoutes.RedeemLocal.route, + arguments = RedeemRoutes.RedeemLocal.arguments + ) { navEntry -> + LocalRedeemScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + + renderComposable( + route = RedeemRoutes.RedeemOnlinePreferences.route, + arguments = RedeemRoutes.RedeemOnlinePreferences.arguments + ) { navEntry -> + OnlineRedeemPreferencesScreen( + navController = navController, + navBackStackEntry = navEntry, + controller = onlineRedeemController + ) + } + + renderComposable( + route = RedeemRoutes.RedeemPrescriptionSelection.route, + arguments = RedeemRoutes.RedeemPrescriptionSelection.arguments + ) { navEntry -> + PrescriptionSelectionScreen( + navController = navController, + navBackStackEntry = navEntry, + controller = onlineRedeemController + ) + } + renderComposable( + route = RedeemRoutes.RedeemOrderOverviewScreen.route, + arguments = RedeemRoutes.RedeemOrderOverviewScreen.arguments + ) { + RedeemOrderOverviewScreen( + navController = navController, + navBackStackEntry = it, + graphController = onlineRedeemController + ) + } + renderComposable( + route = RedeemRoutes.RedeemEditShippingContactScreen.route, + arguments = RedeemRoutes.RedeemEditShippingContactScreen.arguments + ) { + RedeemEditShippingContactScreen( + navController = navController, + navBackStackEntry = it, + graphController = onlineRedeemController + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemRoutes.kt new file mode 100644 index 00000000..d2634fb0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/navigation/RedeemRoutes.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.navigation + +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavType +import androidx.navigation.navArgument +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes +import de.gematik.ti.erp.app.navigation.fromNavigationString +import de.gematik.ti.erp.app.navigation.toNavigationString +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData + +object RedeemRoutes : NavigationRoutes { + override fun subGraphName(): String = "redeem" + + const val REDEEM_NAV_SELECTED_PHARMACY = "Pharmacy" + const val REDEEM_NAV_ORDER_OPTION = "OrderOption" + const val REDEEM_NAV_MODAL_BEHAVIOUR = "ModalBehaviour" + const val REDEEM_NAV_TASK_ID = "taskId" + + object RedeemMethodSelection : Routes(NavigationRouteNames.RedeemMethodSelection.name) + object RedeemLocal : Routes( + NavigationRouteNames.RedeemLocal.name, + navArgument(REDEEM_NAV_TASK_ID) { type = NavType.StringType } + ) { + fun path(taskId: String): String = path(REDEEM_NAV_TASK_ID to taskId) + } + object RedeemOnlinePreferences : Routes(NavigationRouteNames.RedeemOnline.name) + + object RedeemPrescriptionSelection : Routes( + path = NavigationRouteNames.RedeemPrescriptionSelection.name, + navArgument(REDEEM_NAV_MODAL_BEHAVIOUR) { type = NavType.BoolType } + ) { + fun path(isModal: Boolean): String = path(REDEEM_NAV_MODAL_BEHAVIOUR to isModal) + } + + object RedeemOrderOverviewScreen : Routes( + path = NavigationRouteNames.RedeemOrderOverviewScreen.name, + navArgument(REDEEM_NAV_SELECTED_PHARMACY) { type = NavType.StringType }, + navArgument(REDEEM_NAV_ORDER_OPTION) { type = NavType.StringType }, + navArgument(REDEEM_NAV_TASK_ID) { type = NavType.StringType } + + ) { + fun path( + pharmacy: PharmacyUseCaseData.Pharmacy, + orderOption: PharmacyScreenData.OrderOption, + taskId: String? + ): String = path( + REDEEM_NAV_SELECTED_PHARMACY to pharmacy.toNavigationString(), + REDEEM_NAV_ORDER_OPTION to orderOption.name, + REDEEM_NAV_TASK_ID to taskId + ) + } + + object RedeemEditShippingContactScreen : Routes( + path = NavigationRouteNames.RedeemEditShippingContactScreen.name, + navArgument(REDEEM_NAV_ORDER_OPTION) { type = NavType.StringType } + ) { + fun path(orderOption: PharmacyScreenData.OrderOption): String = path(REDEEM_NAV_ORDER_OPTION to orderOption.name) + } +} + +internal class RedeemRouteBackStackEntryArguments( + private val navBackStackEntry: NavBackStackEntry +) { + internal fun getPharmacy(): PharmacyUseCaseData.Pharmacy? = + navBackStackEntry.arguments?.let { bundle -> + bundle.getString(RedeemRoutes.REDEEM_NAV_SELECTED_PHARMACY)?.let { + fromNavigationString(it) + } + } + + internal fun getOrderOption() = + navBackStackEntry.arguments?.let { bundle -> + bundle.getString(RedeemRoutes.REDEEM_NAV_ORDER_OPTION)?.let { + PharmacyScreenData.OrderOption.valueOf(it) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/LocalRedeemScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/LocalRedeemScreenController.kt new file mode 100644 index 00000000..ffcde446 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/LocalRedeemScreenController.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.redeem.usecase.GetDMCodesForLocalRedeemUseCase +import de.gematik.ti.erp.app.redeem.usecase.GetRedeemableTasksForDmCodesUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemScannedTasksUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Suppress("ConstructorParameterNaming") +@Stable +class LocalRedeemScreenController( + private val taskId: String, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val getRedeemableTasksForDmCodesUseCase: GetRedeemableTasksForDmCodesUseCase, + private val getDMCodesForLocalRedeemUseCase: GetDMCodesForLocalRedeemUseCase, + private val redeemScannedTasksUseCase: RedeemScannedTasksUseCase, + private val _prescriptionOrders: MutableStateFlow> = + MutableStateFlow(emptyList()), + private val _showSingleCodes: MutableStateFlow = MutableStateFlow(false), + private val _dmCodes: MutableStateFlow>> = MutableStateFlow(UiState.Loading()) +) : GetActiveProfileController( + getActiveProfileUseCase = getActiveProfileUseCase, + onSuccess = { + profile, coroutineScope -> + coroutineScope.launch { + runCatching { + getRedeemableTasksForDmCodesUseCase(profile.id) + }.fold( + onSuccess = { flowList -> + val list = flowList.first() + if (list.isEmpty()) { + _dmCodes.value = UiState.Empty() + Napier.e { "no redeemable prescriptions found" } + } else { + if (taskId.isNotEmpty()) { + _prescriptionOrders.value = list.filter { it.taskId == taskId } + } else { + _prescriptionOrders.value = list + } + } + }, + onFailure = { + Napier.e { "active prescriptions not found $it" } + } + ) + runCatching { + getDMCodesForLocalRedeemUseCase.invoke(_prescriptionOrders, _showSingleCodes) + }.fold( + onSuccess = { + val list = it.first() + if (list.isEmpty()) { + _dmCodes.value = UiState.Empty() + } else { + _dmCodes.value = UiState.Data(list) + } + }, + onFailure = { + _dmCodes.value = UiState.Error(it) + } + ) + } + }, + onFailure = { throwable, scope -> + scope.launch { + Napier.e { "active profile not found $throwable" } + } + } +) { + val showSingleCodes: StateFlow = _showSingleCodes + val dmCodes: StateFlow>> = _dmCodes + val prescriptionOrders: StateFlow> = _prescriptionOrders + private val _activeProfileId: MutableStateFlow = MutableStateFlow("") + val activeProfileId: StateFlow = _activeProfileId + fun switchSingleCode() { + _showSingleCodes.value = !_showSingleCodes.value + } + + fun redeemPrescriptions() { + controllerScope.launch { + redeemScannedTasksUseCase( + prescriptionOrders.first().map { it.taskId } + ) + } + } + + fun refreshDmCodes() { + _dmCodes.value = UiState.Loading() + getActiveProfile() + getRedeemableTasks() + getDmCodes() + } + + private fun getActiveProfile() { + controllerScope.launch { + runCatching { + getActiveProfileUseCase().first() + }.fold( + onSuccess = { + _activeProfileId.value = it.id + }, + onFailure = { + _dmCodes.value = UiState.Error(it) + } + ) + } + } + + private fun getRedeemableTasks() { + controllerScope.launch { + runCatching { + getRedeemableTasksForDmCodesUseCase(activeProfileId.value) + }.fold( + onSuccess = { flowList -> + val list = flowList.first() + if (list.isEmpty()) { + _dmCodes.value = UiState.Empty() + Napier.e { "no redeemable prescriptions found" } + } else { + if (taskId.isNotEmpty()) { + _prescriptionOrders.value = list.filter { it.taskId == taskId } + } else { + _prescriptionOrders.value = list + } + } + }, + onFailure = { + Napier.e { "active prescriptions not found $it" } + } + ) + } + } + + fun getDmCodes() { + controllerScope.launch { + runCatching { + getDMCodesForLocalRedeemUseCase.invoke(_prescriptionOrders, _showSingleCodes) + }.fold( + onSuccess = { + val list = it.first() + if (list.isEmpty()) { + _dmCodes.value = UiState.Empty() + } else { + _dmCodes.value = UiState.Data(list) + } + }, + onFailure = { + _dmCodes.value = UiState.Error(it) + } + ) + } + } +} + +@Composable +fun rememberLocalRedeemScreenController(taskId: String): LocalRedeemScreenController { + val getActiveProfileUseCase by rememberInstance() + val getRedeemableTasksForDmCodesUseCase by rememberInstance() + val getDMCodesForLocalRedeemUseCase by rememberInstance() + val redeemScannedTasksUseCase by rememberInstance() + return remember { + LocalRedeemScreenController( + taskId, + getActiveProfileUseCase, + getRedeemableTasksForDmCodesUseCase, + getDMCodesForLocalRedeemUseCase, + redeemScannedTasksUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/OnlineRedeemGraphController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/OnlineRedeemGraphController.kt new file mode 100644 index 00000000..e3282227 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/OnlineRedeemGraphController.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.base.presentation.GetActiveProfileController +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.SaveShippingContactUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +abstract class OnlineRedeemGraphController( + getActiveProfileUseCase: GetActiveProfileUseCase +) : GetActiveProfileController(getActiveProfileUseCase) { + abstract val singleTaskId: MutableStateFlow + + abstract fun onResetPrescriptionSelection() + + abstract fun validateAndGetShippingContactState( + contact: PharmacyUseCaseData.ShippingContact?, + selectedOrderOption: PharmacyScreenData.OrderOption? + ): ShippingContactState? + + abstract fun saveShippingContact(contact: PharmacyUseCaseData.ShippingContact) + + @Composable + abstract fun redeemableOrderState(): State> + + @Composable + abstract fun selectedOrderState(): State + + abstract fun onPrescriptionSelectionChanged(order: PharmacyUseCaseData.PrescriptionOrder, select: Boolean) + abstract fun saveSingleTaskId(taskId: String) + abstract suspend fun deselectPrescriptions(taskId: String) +} + +@Stable +class DefaultOnlineRedeemGraphController( + getActiveProfileUseCase: GetActiveProfileUseCase, + private val getOrderStateUseCase: GetOrderStateUseCase, + private val getShippingContactValidationUseCase: GetShippingContactValidationUseCase, + private val saveShippingContactUseCase: SaveShippingContactUseCase +) : OnlineRedeemGraphController(getActiveProfileUseCase) { + + override val singleTaskId by lazy { MutableStateFlow("") } + + private val orders by lazy { getOrderStateUseCase() } + + private val unselectedPrescriptionTaskIds: MutableStateFlow> by lazy { + MutableStateFlow(emptyList()) + } + + private val prescriptionOrders by lazy { + orders.map { + it.prescriptionOrders + } + } + + private val selectedOrders by lazy { + Napier.d { "--- selected orders changed ---" } + combine( + unselectedPrescriptionTaskIds, + orders + ) { unSelectedPrescriptions, orderState -> + orderState.copy( + prescriptionOrders = orderState.prescriptionOrders.filter { it.taskId !in unSelectedPrescriptions } + ) + } + } + + override fun onPrescriptionSelectionChanged(order: PharmacyUseCaseData.PrescriptionOrder, select: Boolean) { + if (!select) { + unselectedPrescriptionTaskIds.update { it + order.taskId } + } else { + unselectedPrescriptionTaskIds.update { it - order.taskId } + } + } + + override fun saveSingleTaskId(taskId: String) { + singleTaskId.value = taskId + } + + override suspend fun deselectPrescriptions(taskId: String) { + if (taskId != singleTaskId.value) { + selectedOrders.first().prescriptionOrders.forEach { + if (it.taskId != taskId) { + onPrescriptionSelectionChanged(it, false) + } + } + } + saveSingleTaskId(taskId) + } + + override fun onResetPrescriptionSelection() { + unselectedPrescriptionTaskIds.value = emptyList() + singleTaskId.value = "" + } + + override fun validateAndGetShippingContactState( + contact: PharmacyUseCaseData.ShippingContact?, + selectedOrderOption: PharmacyScreenData.OrderOption? + ): ShippingContactState? = contact?.let { getShippingContactValidationUseCase(it, selectedOrderOption) } + + override fun saveShippingContact(contact: PharmacyUseCaseData.ShippingContact) { + controllerScope.launch { + saveShippingContactUseCase(contact) + } + } + + // all orders that are available for redeeming + @Composable + override fun redeemableOrderState() = prescriptionOrders.collectAsStateWithLifecycle(emptyList()) + + // all orders that are selected for redeeming + @Composable + override fun selectedOrderState() = + selectedOrders.collectAsStateWithLifecycle(PharmacyUseCaseData.OrderState.Empty) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemOrderOverviewScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemOrderOverviewScreenController.kt new file mode 100644 index 00000000..cf99f3dd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemOrderOverviewScreenController.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.model.ChooseAuthenticationController +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.core.LocalBiometricAuthenticator +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.kodein.di.compose.rememberInstance + +class RedeemOrderOverviewScreenController( + getProfileByIdUseCase: GetProfileByIdUseCase, + getProfilesUseCase: GetProfilesUseCase, + getActiveProfileUseCase: GetActiveProfileUseCase, + chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase, + biometricAuthenticator: BiometricAuthenticator +) : ChooseAuthenticationController( + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + biometricAuthenticator = biometricAuthenticator +) { + private val _isProfileRefreshing: MutableStateFlow = MutableStateFlow(false) + + val onBiometricAuthenticationSuccessEvent = ComposableEvent() + val showAuthenticationErrorDialog = ComposableEvent() + val isProfileRefreshing = _isProfileRefreshing.asStateFlow() + + init { + biometricAuthenticationSuccessEvent.listen(controllerScope) { + onBiometricAuthenticationSuccessEvent.trigger() + } + + biometricAuthenticationResetErrorEvent.listen(controllerScope) { error -> + showAuthenticationErrorDialog.trigger(error) + } + + biometricAuthenticationOtherErrorEvent.listen(controllerScope) { error -> + showAuthenticationErrorDialog.trigger(error) + } + + onRefreshProfileAction.listen(controllerScope) { isRefreshing -> + _isProfileRefreshing.value = isRefreshing + } + } +} + +@Composable +fun rememberOrderOverviewScreenController(): RedeemOrderOverviewScreenController { + val biometricAuthenticator = LocalBiometricAuthenticator.current + val getProfilesUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val getProfileByIdUseCase by rememberInstance() + + val chooseAuthenticationDataUseCase by rememberInstance() + + return remember { + RedeemOrderOverviewScreenController( + biometricAuthenticator = biometricAuthenticator, + getProfilesUseCase = getProfilesUseCase, + getProfileByIdUseCase = getProfileByIdUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsController.kt new file mode 100644 index 00000000..29e56ef1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsController.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.pharmacy.model.PrescriptionRedeemArguments +import de.gematik.ti.erp.app.pharmacy.model.PrescriptionRedeemArguments.DirectRedemptionArguments +import de.gematik.ti.erp.app.pharmacy.model.PrescriptionRedeemArguments.LoggedInUserRedemptionArguments +import de.gematik.ti.erp.app.redeem.model.RedeemedPrescriptionState +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnDirectUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnLoggedInUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class RedeemPrescriptionsController( + private val redeemPrescriptionsOnLoggedInUseCase: RedeemPrescriptionsOnLoggedInUseCase, + private val redeemPrescriptionsOnDirectUseCase: RedeemPrescriptionsOnDirectUseCase +) : Controller() { + + val onProcessStartEvent: ComposableEvent = ComposableEvent() + val onProcessEndEvent: ComposableEvent = ComposableEvent() + + private var _redeemedState = MutableStateFlow( + RedeemedPrescriptionState.Init + ) + val redeemedState = _redeemedState.asStateFlow() + + // process prescription redemptions and update the state + fun processPrescriptionRedemptions( + arguments: PrescriptionRedeemArguments + ) { + arguments.onRedemptionState( + directRedemptionBlock = { + controllerScope.launch { + processPrescriptionForDirectRedemption(it) + } + }, + loggedInUserRedemptionBlock = { + controllerScope.launch { + processPrescriptionRedemptionsForLoggedInUser(it) + } + } + ) + } + + private suspend fun processPrescriptionRedemptionsForLoggedInUser( + arguments: LoggedInUserRedemptionArguments + ) { + redeemPrescriptionsOnLoggedInUseCase.invoke( + orderId = arguments.orderId, + profileId = arguments.profile.id, + redeemOption = arguments.redeemOption, + prescriptionOrderInfos = arguments.prescriptionOrderInfos, + contact = arguments.contact, + pharmacy = arguments.pharmacy, + onProcessStart = { onProcessStartEvent.trigger() }, + onProcessEnd = { onProcessEndEvent.trigger() } + ).collectLatest { value -> + _redeemedState.value = value + } + } + + @Requirement( + "A_22778-01#1", + "A_22779-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Start Redeem without TI (Controller)." + ) + private suspend fun processPrescriptionForDirectRedemption( + arguments: DirectRedemptionArguments + ) { + redeemPrescriptionsOnDirectUseCase.invoke( + orderId = arguments.orderId, + redeemOption = arguments.redeemOption, + prescriptionOrderInfos = arguments.prescriptionOrderInfos, + contact = arguments.contact, + pharmacy = arguments.pharmacy, + onProcessStart = { onProcessStartEvent.trigger() }, + onProcessEnd = { onProcessEndEvent.trigger() } + ).collectLatest { + _redeemedState.value = it + } + } +} + +@Composable +fun rememberRedeemPrescriptionsController(): RedeemPrescriptionsController { + val redeemPrescriptionsOnLoggedInUseCase by rememberInstance() + val redeemPrescriptionsOnDirectUseCase by rememberInstance() + + return remember { + RedeemPrescriptionsController( + redeemPrescriptionsOnLoggedInUseCase = redeemPrescriptionsOnLoggedInUseCase, + redeemPrescriptionsOnDirectUseCase = redeemPrescriptionsOnDirectUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt deleted file mode 100644 index bd09ab57..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.ui.FlatButton -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge - -@Composable -fun HowToRedeem( - onClickLocalRedeem: () -> Unit, - onClickOnlineRedeem: () -> Unit, - onCancel: () -> Unit -) { - val scrollState = rememberScrollState() - val elevated by remember { - derivedStateOf { scrollState.value > 0 } - } - AnimatedElevationScaffold( - topBarTitle = "", - navigationMode = NavigationBarMode.Close, - elevated = elevated, - onBack = onCancel, - actions = {} - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.weight(1f)) - Image(painterResource(R.drawable.order_onb_pharmacist_blue), null) - Spacer(Modifier.height(80.dp)) - Text( - stringResource(R.string.order_onb_how_to_title), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.order_onb_how_to_desc), - style = AppTheme.typography.subtitle2l, - textAlign = TextAlign.Center - ) - SpacerXXLarge() - TextFlatButton( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.order_onb_how_to_local), - description = stringResource(R.string.order_onb_how_to_local_desc), - onClick = onClickLocalRedeem - ) - SpacerMedium() - TextFlatButton( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.order_onb_how_to_online), - description = stringResource(R.string.order_onb_how_to_online_desc), - onClick = onClickOnlineRedeem - ) - Spacer(Modifier.navigationBarsPadding()) - } - } -} - -@Composable -fun TextFlatButton( - modifier: Modifier = Modifier, - title: String, - description: String? = null, - onClick: () -> Unit -) = - FlatButton( - modifier = modifier, - onClick = onClick - ) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - Text(title, style = AppTheme.typography.subtitle1) - description?.let { - SpacerTiny() - Text(description, style = AppTheme.typography.body2l) - } - } - SpacerMedium() - Icon(Icons.Rounded.KeyboardArrowRight, null) - } - } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt deleted file mode 100644 index 1c66c49d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import android.annotation.SuppressLint -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.PagerState -import com.google.accompanist.pager.rememberPagerState -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.DataMatrix -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar -import de.gematik.ti.erp.app.utils.compose.TertiaryButton -import de.gematik.ti.erp.app.utils.compose.createBitMatrix -import de.gematik.ti.erp.app.utils.extensions.forceBrightness -import kotlinx.coroutines.launch - -@Requirement( - "A_20181", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Only the title of the prescription and the DMC consisting of Task-ID and Access-Code are displayed." -) -@SuppressLint("UnusedMaterialScaffoldPaddingParameter") -@Composable -fun LocalRedeemScreen( - activeProfile: ProfilesUseCaseData.Profile, - orderState: PharmacyOrderController, - onBack: () -> Unit, - onFinished: () -> Unit -) { - val localRedeemState = rememberLocalRedeemState(orderState) - val codes by localRedeemState.codes - - if (codes.isEmpty()) { - return - } - - val activity = LocalActivity.current - activity.forceBrightness() - - val redeemController = rememberRedeemController(activeProfile) - var showRedeemScannedDialog by remember { mutableStateOf(false) } - if (showRedeemScannedDialog) { - val scope = rememberCoroutineScope() - val prescriptions by orderState.prescriptionsState - RedeemScannedPrescriptionsDialog( - onClickRedeem = { - scope.launch { - redeemController.redeemScannedTasks(prescriptions.map { it.taskId }) - onFinished() - } - }, - onCancel = onFinished - ) - } - - Scaffold( - topBar = { - NavigationTopAppBar( - navigationMode = NavigationBarMode.Back, - elevation = 0.dp, - title = "", - onBack = onBack, - actions = { - TextButton( - onClick = { - if (codes.any { it.containsScanned }) { - showRedeemScannedDialog = true - } else { - onFinished() - } - } - ) { - Text(stringResource(R.string.local_redeem_done)) - } - } - ) - } - ) { innerPadding -> - val pagerState = rememberPagerState() - - Column( - Modifier - .padding(innerPadding) - .fillMaxSize() - ) { - HorizontalPager( - count = codes.size, - state = pagerState - ) { page -> - DataMatrix( - modifier = Modifier - .padding(PaddingDefaults.Medium) - .fillMaxWidth(), - code = codes[page] - ) - } - if (codes.size > 1 || (codes.size == 1 && codes.first().nrOfCodes > 1)) { - PageIndicator( - modifier = Modifier.align(Alignment.CenterHorizontally), - pagerState = pagerState - ) - Spacer(Modifier.weight(1f)) - TertiaryButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = PaddingDefaults.XXLarge) - .navigationBarsPadding(), - onClick = { - if (localRedeemState.isSingleCodes) { - localRedeemState.onSwitchToGroupedCodes() - } else { - localRedeemState.onSwitchToSingleCodes() - } - } - ) { - if (localRedeemState.isSingleCodes) { - Text(stringResource(R.string.local_redeem_grouped_codes)) - } else { - Text(stringResource(R.string.local_redeem_single_codes)) - } - } - } - } - } -} - -@Composable -fun DataMatrix( - modifier: Modifier, - code: LocalRedeemState.DMCode -) { - val matrix = remember(code) { createBitMatrix(code.payload) } - - DataMatrix(modifier, matrix, code.name) -} - -@Requirement( - "A_19183#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "User displays a DMC to redeem a prescription in a pharmacy." -) -@Composable -private fun PageIndicator( - modifier: Modifier, - pagerState: PagerState -) { - Box( - modifier = modifier - .clip(RoundedCornerShape(8.dp)) - .background(AppTheme.colors.neutral100) - .padding(vertical = PaddingDefaults.Small, horizontal = PaddingDefaults.Medium) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - repeat(pagerState.pageCount) { - Dot(color = AppTheme.colors.neutral300) - } - } - - val fraction = pagerState.currentPage + pagerState.currentPageOffset - val offsetX = with(LocalDensity.current) { - val gap = PaddingDefaults.Small.roundToPx() - val size = 8.dp.roundToPx() - (fraction * (size + gap)).toDp() - } - Dot(modifier = Modifier.offset(x = offsetX), color = AppTheme.colors.primary500) - } -} - -@Composable -private fun Dot(modifier: Modifier = Modifier, color: Color) { - Box( - modifier = modifier - .clip(CircleShape) - .background(color) - .size(8.dp) - ) -} - -@Composable -private fun RedeemScannedPrescriptionsDialog(onClickRedeem: () -> Unit, onCancel: () -> Unit) = - CommonAlertDialog( - header = stringResource(R.string.redeem_prescriptions_dialog_header), - info = stringResource(R.string.redeem_prescriptions_dialog_info), - cancelText = stringResource(R.string.redeem_prescriptions_dialog_cancel), - actionText = stringResource(R.string.redeem_prescriptions_dialog_redeem), - onCancel = onCancel, - onClickAction = onClickRedeem - ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt deleted file mode 100644 index c76834a4..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.utils.createDMPayload - -@Stable -class LocalRedeemState( - private val prescriptions: State> -) { - @Immutable - data class DMCode( - val payload: String, - val nrOfCodes: Int, - val name: String?, - val containsScanned: Boolean - ) - - var isSingleCodes by mutableStateOf(false) - private set - - fun onSwitchToSingleCodes() { - isSingleCodes = true - } - - fun onSwitchToGroupedCodes() { - isSingleCodes = false - } - - val codes - @Composable - get() = derivedStateOf { - val prescriptions = prescriptions.value - - @Suppress("MagicNumber") - val maxTasks = if (isSingleCodes) { - 1 - } else { - if (prescriptions.size < 5) { - 2 - } else { - 3 - } - } - - prescriptions - .map { prescription -> - prescription to "Task/${prescription.taskId}/\$accept?ac=${prescription.accessCode}" - } - .windowed(maxTasks, maxTasks, partialWindows = true) - .map { codes -> - val prescriptions = codes.map { it.first } - val urls = codes.map { it.second } - val json = createDMPayload(urls) - DMCode( - payload = json, - nrOfCodes = urls.size, - name = prescriptions.filter { it.title != null }.joinToString { it.title!! }, - containsScanned = prescriptions.any { it.title == null } - ) - } - } -} - -@Composable -fun rememberLocalRedeemState( - orderState: PharmacyOrderController -): LocalRedeemState { - val prescriptions = orderState.prescriptionsState - return remember { - LocalRedeemState( - prescriptions - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt deleted file mode 100644 index 8f241c38..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync -import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyOrderController -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyNavigation -import de.gematik.ti.erp.app.pharmacy.ui.PrescriptionSelection -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.redeem.ui.model.RedeemNavigation -import de.gematik.ti.erp.app.utils.compose.NavigationAnimation -import de.gematik.ti.erp.app.utils.compose.navigationModeState - -@Composable -fun RedeemNavigation( - onFinish: () -> Unit -) { - val orderState = rememberPharmacyOrderController() - - val navController = rememberNavController() - val navigationMode by navController.navigationModeState(RedeemNavigation.MethodSelection.route) - - var previousNavEntry by remember { mutableStateOf("redeem_methodSelection") } - trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - - NavHost( - navController, - startDestination = RedeemNavigation.MethodSelection.route - ) { - composable(RedeemNavigation.MethodSelection.route) { - NavigationAnimation(mode = navigationMode) { - val prescriptions by orderState.prescriptionsState - HowToRedeem( - onClickLocalRedeem = { - navController.navigate(RedeemNavigation.LocalRedeem.path()) - }, - onClickOnlineRedeem = { - if (prescriptions.size > 1) { - navController.navigate(RedeemNavigation.OnlineRedeem.path()) - } else { - navController.navigate(RedeemNavigation.PharmacySearch.path()) - } - }, - onCancel = onFinish - ) - } - } - composable(RedeemNavigation.LocalRedeem.route) { - val profilesController = rememberProfileController() - val activeProfile by profilesController.getActiveProfileState() - NavigationAnimation(mode = navigationMode) { - LocalRedeemScreen( - activeProfile = activeProfile, - onBack = { - navController.popBackStack() - }, - onFinished = onFinish, - orderState = orderState - ) - } - } - composable(RedeemNavigation.OnlineRedeem.route) { - NavigationAnimation(mode = navigationMode) { - OnlineRedeemScreen( - orderState = orderState, - onClickSelectPrescriptions = { - orderState.onResetPrescriptionSelection() - navController.navigate(RedeemNavigation.PrescriptionSelection.path()) - }, - onClickAllPrescriptions = { - orderState.onResetPrescriptionSelection() - navController.navigate(RedeemNavigation.PharmacySearch.path()) - }, - onBack = { - navController.popBackStack() - } - ) - } - } - composable(RedeemNavigation.PrescriptionSelection.route) { - NavigationAnimation(mode = navigationMode) { - PrescriptionSelection( - orderState = orderState, - onFinishSelection = { - navController.navigate(RedeemNavigation.PharmacySearch.path()) - }, - showNextButton = true, - backIsFinish = false, - onBack = { - navController.popBackStack() - } - ) - } - } - composable(RedeemNavigation.PharmacySearch.route) { - PharmacyNavigation( - isNestedNavigation = true, - pharmacyOrderController = orderState, - onBack = { - navController.popBackStack() - }, - onFinish = onFinish - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt deleted file mode 100644 index 41c7c055..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge - -@Composable -fun OnlineRedeemScreen( - orderState: PharmacyOrderController, - onClickSelectPrescriptions: () -> Unit, - onClickAllPrescriptions: () -> Unit, - onBack: () -> Unit -) { - val prescriptions by orderState.prescriptionsState - val scrollState = rememberScrollState() - val elevated by remember { - derivedStateOf { scrollState.value > 0 } - } - AnimatedElevationScaffold( - topBarTitle = "", - navigationMode = NavigationBarMode.Back, - elevated = elevated, - onBack = onBack, - actions = {} - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.weight(1f)) - Image(painterResource(R.drawable.order_onb_blue_handoutmedicine), null) - Spacer(Modifier.height(80.dp)) - Text( - stringResource(R.string.online_redeem_choose_rx_title), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - pluralStringResource(R.plurals.online_redeem_rx_desc, prescriptions.size, prescriptions.size), - style = AppTheme.typography.subtitle2l, - textAlign = TextAlign.Center - ) - SpacerXXLarge() - TextFlatButton( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.online_redeem_choose_rx), - onClick = onClickSelectPrescriptions - ) - SpacerMedium() - TextFlatButton( - modifier = Modifier.fillMaxWidth(), - title = stringResource(R.string.online_redeem_choose_all_rx), - onClick = onClickAllPrescriptions - ) - Spacer(Modifier.navigationBarsPadding()) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt deleted file mode 100644 index 9215ce25..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.redeem.usecase.RedeemUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.shareIn -import org.kodein.di.compose.rememberInstance - -@Stable -class RedeemController( - scope: CoroutineScope, - val profileId: ProfileIdentifier, - private val useCase: RedeemUseCase -) { - private val hasRedeemableTasksFlow = - useCase - .hasRedeemablePrescriptions(profileId) - .shareIn(scope, SharingStarted.Lazily, 1) - - val hasRedeemableTasks - @Composable - get() = hasRedeemableTasksFlow.collectAsStateWithLifecycle(false) - - suspend fun redeemScannedTasks(taskIds: List) { - useCase.redeemScannedTasks(taskIds) - } -} - -@Composable -fun rememberRedeemController( - activeProfile: ProfilesUseCaseData.Profile -): RedeemController { - val useCase by rememberInstance() - val scope = rememberCoroutineScope() - return remember(activeProfile.id) { - RedeemController( - scope, - activeProfile.id, - useCase - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCode.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCode.kt new file mode 100644 index 00000000..c4347618 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCode.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.utils.compose.DataMatrix +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.createBitMatrix + +@Composable +fun DataMatrixCode( + modifier: Modifier, + code: DMCode +) { + val matrix = remember(code) { createBitMatrix(code.payload) } + + DataMatrix(modifier, matrix, code.name) +} + +@LightDarkPreview +@Composable +fun DataMatrixCodePreview() { + DataMatrixCode( + modifier = Modifier, + code = DMCode( + payload = "1213233647678679789790", + nrOfCodes = 1, + name = "Medication", + selfPayerPrescriptionNames = listOf(), + containsScanned = false + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCodesWithSelfPayerWarning.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCodesWithSelfPayerWarning.kt new file mode 100644 index 00000000..05a69dd4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/DataMatrixCodesWithSelfPayerWarning.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import de.gematik.ti.erp.app.animated.AnimationTime +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import kotlinx.coroutines.delay + +@Composable +fun DataMatrixCodesWithSelfPayerWarning( + dmCode: DMCode, + showSingleCodes: Boolean, + sharedWarningHeight: Int, + onSharedWarningHeightUpdated: (Int) -> Unit +) { + var heightIsVisible by remember { mutableStateOf(true) } + + // Animate the height of the spacer + val animatedSpacerHeight by animateDpAsState( + label = "DataMatrixCodesWithSelfPayerWarningSpacerHeight", + targetValue = if (dmCode.selfPayerPrescriptionNames.isEmpty() && heightIsVisible) { + with(LocalDensity.current) { sharedWarningHeight.toDp() } + } else { + SizeDefaults.zero + }, + animationSpec = tween() + ) + + LaunchedEffect(Unit) { + if (dmCode.selfPayerPrescriptionNames.isEmpty()) { + delay(AnimationTime.SHORT_DELAY) + heightIsVisible = false + onSharedWarningHeightUpdated(0) + } + } + + Column( + modifier = Modifier.animateContentSize() + ) { + AnimatedVisibility( + visible = dmCode.selfPayerPrescriptionNames.isNotEmpty(), + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically() + ) { + SelfPayerPrescriptionWarning( + modifier = Modifier.onGloballyPositioned { layoutCoordinates -> + val height = layoutCoordinates.size.height + // Update the shared state with the new height if it's larger + if (height > sharedWarningHeight) { + onSharedWarningHeightUpdated(height) + } + }, + selfPayerPrescriptionNames = dmCode.selfPayerPrescriptionNames, + showSingleCodes = showSingleCodes, + nrOfDMCodes = dmCode.nrOfCodes + ) + } + Spacer(modifier = Modifier.height(animatedSpacerHeight)) + DataMatrixCode( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth(), + code = dmCode + ) + } +} + +@LightDarkPreview +@Composable +fun DataMatrixCodesWithSelfPayerWarningPreview() { + PreviewAppTheme { + DataMatrixCodesWithSelfPayerWarning( + dmCode = DMCode( + payload = "1213233647678679789790", + nrOfCodes = 1, + name = "Medication", + selfPayerPrescriptionNames = listOf("Medication"), + containsScanned = false + ), + showSingleCodes = false, + sharedWarningHeight = 0, + onSharedWarningHeightUpdated = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/RedeemButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/RedeemButton.kt new file mode 100644 index 00000000..8478dcc8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/RedeemButton.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.model.PrescriptionRedeemArguments.Companion.from +import de.gematik.ti.erp.app.pharmacy.model.orderID +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.redeem.model.RedeemDialogParameters +import de.gematik.ti.erp.app.redeem.model.RedeemPrescriptionDialogMessageState +import de.gematik.ti.erp.app.redeem.model.RedeemPrescriptionDialogMessageState.Companion.toDialogMessageState +import de.gematik.ti.erp.app.redeem.model.RedeemedPrescriptionState +import de.gematik.ti.erp.app.redeem.presentation.rememberRedeemPrescriptionsController +import de.gematik.ti.erp.app.redeem.ui.screens.PrescriptionRedeemAlertDialog +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerXLarge +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonLarge +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.letNotNull + +// A composable button that can handle the redemption process and gives the user feedback about the process +@Suppress("CyclomaticComplexMethod") +@Composable +fun RedeemButton( + profile: ProfilesUseCaseData.Profile, + order: PharmacyUseCaseData.OrderState, + selectedPharmacy: PharmacyUseCaseData.Pharmacy, + selectedOrderOption: PharmacyScreenData.OrderOption, + shippingContactCompleted: Boolean, + isRedemptionPossible: Boolean, + onNotRedeemable: () -> Unit, + onFinish: (Boolean) -> Unit, + onProcessStarted: () -> Unit, + onProcessEnded: () -> Unit +) { + val dialog = LocalDialog.current + + val showDialogEvent: ComposableEvent = remember { ComposableEvent() } + + val redeemController = rememberRedeemPrescriptionsController() + + val redeemedState by redeemController.redeemedState.collectAsStateWithLifecycle() + + val processStartedEvent = redeemController.onProcessStartEvent + val processEndEvent = redeemController.onProcessEndEvent + + var uploadInProgress by remember { mutableStateOf(false) } + var orderHasError by remember { mutableStateOf(false) } + + LaunchedEffect(redeemedState) { + redeemedState.isOrderCompletedState { state -> + try { + orderHasError = state.results.values.containsError() + val dialogMessageState = obtainDialogParameters(state.results.values) + showDialogEvent.trigger(dialogMessageState.redeemDialogParameters) + } catch (e: Throwable) { + showDialogEvent.trigger(RedeemPrescriptionDialogMessageState.Unknown().redeemDialogParameters) + } + } + } + + processStartedEvent.listen { + uploadInProgress = true + onProcessStarted() + } + + processEndEvent.listen { + uploadInProgress = false + onProcessEnded() + } + + showDialogEvent.listen { dialogParams -> + dialog.show { + PrescriptionRedeemAlertDialog( + title = stringResource(dialogParams.title), + description = stringResource(dialogParams.description), + onDismiss = { + it.dismiss() + onFinish(orderHasError) + } + ) + } + } + + Surface( + modifier = Modifier.fillMaxWidth(), + elevation = SizeDefaults.half + ) { + Column(Modifier.navigationBarsPadding()) { + SpacerMedium() + PrimaryButtonLarge( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton), + enabled = shippingContactCompleted && !uploadInProgress, + onClick = { + if (isRedemptionPossible) { + letNotNull( + first = selectedOrderOption, + second = selectedPharmacy + ) { orderOption, pharmacy -> + redeemController.processPrescriptionRedemptions( + arguments = orderID().from( + profile = profile, + order = order, + redeemOption = orderOption, + pharmacy = pharmacy + ) + ) + } + } else { + onNotRedeemable() + } + } + ) { + Text(stringResource(R.string.pharmacy_order_send)) + } + SpacerXLarge() + } + } +} + +private fun obtainDialogParameters( + results: Collection +): RedeemPrescriptionDialogMessageState = + when { + // case 1: When one prescription is transferred. + results.size == 1 -> results.firstNotNullOf { it?.toDialogMessageState() } + + // case 2.1: When multiple prescriptions are transferred Successfully. + results.containsNoError() -> RedeemPrescriptionDialogMessageState.Success() + + // case 2.2: When any multiple prescription are transferred Unsuccessfully. Show a generic error message. + else -> RedeemPrescriptionDialogMessageState.MultiplePrescriptionsFailed() + } + +private fun Collection.containsError(): Boolean = + any { it is RedeemedPrescriptionState.Error } + +private fun Collection.containsNoError(): Boolean = + any { it !is RedeemedPrescriptionState.Error } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/SelfPayerPrescriptionWarning.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/SelfPayerPrescriptionWarning.kt new file mode 100644 index 00000000..136ed5bb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/SelfPayerPrescriptionWarning.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.WarningAmber +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.Banner +import de.gematik.ti.erp.app.utils.compose.BannerClickableIcon +import de.gematik.ti.erp.app.utils.compose.BannerIcon +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource + +@Composable +fun SelfPayerPrescriptionWarning( + modifier: Modifier = Modifier, + selfPayerPrescriptionNames: List, + showSingleCodes: Boolean, + nrOfDMCodes: Int +) { + if (selfPayerPrescriptionNames.isNotEmpty()) { + val text = if (showSingleCodes && selfPayerPrescriptionNames.size == 1 || + nrOfDMCodes == 1 + ) { + stringResource( + R.string.local_redeem_self_payer_prescription_warning + ) + } else { + annotatedPluralsResource( + R.plurals.pharmacy_order_self_payer_prescriptions, + selfPayerPrescriptionNames.size, + AnnotatedString( + selfPayerPrescriptionNames.joinToString(" & ") + ) + ) + } + Banner( + modifier = Modifier + .padding(PaddingDefaults.Medium) + .then(modifier), + startIcon = BannerClickableIcon( + BannerIcon.Custom( + vector = Icons.Rounded.WarningAmber, + color = AppTheme.colors.yellow900 + ), + onClick = {} + ), + contentColor = AppTheme.colors.yellow900, + containerColor = AppTheme.colors.yellow100, + borderColor = AppTheme.colors.yellow900, + text = text.toString() + ) + } +} + +@Composable +fun SelfPayerPrescriptionWarning( + selfPayerPrescriptionNames: List +) { + Banner( + modifier = Modifier.padding(PaddingDefaults.Medium), + startIcon = BannerClickableIcon( + BannerIcon.Custom( + vector = Icons.Rounded.WarningAmber, + color = AppTheme.colors.yellow900 + ), + onClick = {} + ), + contentColor = AppTheme.colors.yellow900, + containerColor = AppTheme.colors.yellow100, + borderColor = AppTheme.colors.yellow900, + text = annotatedPluralsResource( + R.plurals.pharmacy_order_self_payer_prescriptions, + selfPayerPrescriptionNames.size, + AnnotatedString(selfPayerPrescriptionNames.joinToString(" & ")) + ).text + ) +} + +@LightDarkPreview +@Composable +fun SelfPayerPrescriptionWarningPreview() { + SelfPayerPrescriptionWarning( + selfPayerPrescriptionNames = listOf("Medication"), + nrOfDMCodes = 2, + showSingleCodes = false + ) +} + +@LightDarkPreview +@Composable +fun SelfPayerPrescriptionWarningSingleCodesPreview() { + SelfPayerPrescriptionWarning( + selfPayerPrescriptionNames = listOf("Medication"), + nrOfDMCodes = 1, + showSingleCodes = true + ) +} + +@LightDarkPreview +@Composable +fun SelfPayerPrescriptionWarningListPreview() { + SelfPayerPrescriptionWarning(selfPayerPrescriptionNames = listOf("Medication")) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/TextFlatButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/TextFlatButton.kt new file mode 100644 index 00000000..49e06f3e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/components/TextFlatButton.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.redeem.ui.screens.FlatButton +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny + +@Suppress("FunctionNaming") +@Composable +fun TextFlatButton( + modifier: Modifier = Modifier, + title: String, + description: String? = null, + onClick: () -> Unit +) = + FlatButton( + modifier = modifier, + onClick = onClick + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text(title, style = AppTheme.typography.subtitle1) + description?.let { + SpacerTiny() + Text(description, style = AppTheme.typography.body2l) + } + } + SpacerMedium() + Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, null) + } + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt deleted file mode 100644 index 88d8711e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.ui.model - -import de.gematik.ti.erp.app.navigation.Routes - -class RedeemNavigation { - object MethodSelection : Routes("redeem_methodSelection") - object PrescriptionSelection : Routes("redeem_prescriptionChooseSubset") - object LocalRedeem : Routes("redeem_matrixCode") - object OnlineRedeem : Routes("redeem_prescriptionAllOrSelection") - object PharmacySearch : Routes("pharmacySearch") -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/LocalRedeemPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/LocalRedeemPreviewParameter.kt new file mode 100644 index 00000000..1c838dcb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/LocalRedeemPreviewParameter.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.utils.uistate.UiState + +data class LocalRedeemPreview( + val name: String, + val dmCodes: UiState>, + val showSingleCodes: Boolean +) + +const val PAYLOAD = "payload" +const val MEDICATION_NAME_1 = "Medication 1" +const val MEDICATION_NAME_2 = "Medication 2" + +class LocalRedeemPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + LocalRedeemPreview( + name = "EmptyState", + dmCodes = UiState.Empty(), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "ErrorState", + dmCodes = UiState.Error(Throwable("Error")), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "LoadingState", + dmCodes = UiState.Loading(), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "SingleDataMatrixCodeWithSelfPayerWarning", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 1, + name = MEDICATION_NAME_1, + selfPayerPrescriptionNames = listOf(MEDICATION_NAME_1), + containsScanned = false + ) + ) + ), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "MultipleDataMatrixCodeWithSelfPayerWarning_Single_Codes", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 2, + name = "$MEDICATION_NAME_1, $MEDICATION_NAME_2", + selfPayerPrescriptionNames = listOf(MEDICATION_NAME_1), + containsScanned = false + ) + ) + ), + showSingleCodes = true + ), + LocalRedeemPreview( + name = "MultipleDataMatrixCodeWithOneSelfPayerWarning", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 2, + name = "$MEDICATION_NAME_1, $MEDICATION_NAME_2", + selfPayerPrescriptionNames = listOf(MEDICATION_NAME_1), + containsScanned = false + ) + ) + ), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "MultipleDataMatrixCodeWithTwoSelfPayerWarning", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 2, + name = "$MEDICATION_NAME_1, $MEDICATION_NAME_2", + selfPayerPrescriptionNames = listOf(MEDICATION_NAME_1, MEDICATION_NAME_2), + containsScanned = false + ) + ) + ), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "SingleDataMatrixCode", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 1, + name = MEDICATION_NAME_1, + selfPayerPrescriptionNames = listOf(), + containsScanned = false + ) + ) + ), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "MultipleDataMatrixCode_Single_Codes_false", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 2, + name = "$MEDICATION_NAME_1, $MEDICATION_NAME_2", + selfPayerPrescriptionNames = listOf(), + containsScanned = false + ) + ) + ), + showSingleCodes = false + ), + LocalRedeemPreview( + name = "MultipleDataMatrixCode_Single_Codes", + dmCodes = UiState.Data( + listOf( + DMCode( + payload = PAYLOAD, + nrOfCodes = 2, + name = "$MEDICATION_NAME_1, $MEDICATION_NAME_2", + selfPayerPrescriptionNames = listOf(), + containsScanned = false + ) + ) + ), + showSingleCodes = true + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/RedeemEditShippingPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/RedeemEditShippingPreviewParameter.kt new file mode 100644 index 00000000..6f6768e3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/preview/RedeemEditShippingPreviewParameter.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData + +data class ShippingContactPreviewData( + val name: String, + val validShippingContactState: ShippingContactState.ValidShippingContactState? = null, + val invalidShippingContactState: ShippingContactState.InvalidShippingContactState? = null, + val errorShippingContactState: ShippingContactState.InvalidShippingContactState? = null, + val shippingContact: PharmacyUseCaseData.ShippingContact +) + +class RedeemEditShippingPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + ShippingContactPreviewData( + name = "ValidShippingState", + validShippingContactState = ShippingContactState.ValidShippingContactState.OK, + shippingContact = PharmacyUseCaseData.ShippingContact( + name = "John Doe", + line1 = "123 Main St", + line2 = "Apt 4B", + postalCode = "12345", + city = "Metropolis", + telephoneNumber = "555-1234", + mail = "john.doe@example.com", + deliveryInformation = "Leave at the front door" + ) + ), + ShippingContactPreviewData( + name = "InvalidShippingState", + invalidShippingContactState = ShippingContactState.InvalidShippingContactState( + errorList = listOf( + ShippingContactState.ShippingContactError.InvalidName, + ShippingContactState.ShippingContactError.InvalidLine1, + ShippingContactState.ShippingContactError.InvalidLine2, + ShippingContactState.ShippingContactError.InvalidPostalCode, + ShippingContactState.ShippingContactError.InvalidCity, + ShippingContactState.ShippingContactError.InvalidPhoneNumber, + ShippingContactState.ShippingContactError.InvalidMail, + ShippingContactState.ShippingContactError.InvalidDeliveryInformation + ) + ), + shippingContact = PharmacyUseCaseData.ShippingContact( + name = "!@#$%^&*()", + line1 = "@#@#", + line2 = "#$#$#", + postalCode = "1", + city = "@", + telephoneNumber = "#$#$", + mail = "", + deliveryInformation = "!!#@!@#" + ) + ), + ShippingContactPreviewData( + name = "ErrorShippingState", + errorShippingContactState = ShippingContactState.InvalidShippingContactState( + errorList = listOf( + ShippingContactState.ShippingContactError.EmptyName, + ShippingContactState.ShippingContactError.EmptyLine1, + ShippingContactState.ShippingContactError.EmptyPostalCode, + ShippingContactState.ShippingContactError.EmptyCity, + ShippingContactState.ShippingContactError.EmptyPhoneNumber, + ShippingContactState.ShippingContactError.EmptyMail + ) + ), + shippingContact = PharmacyUseCaseData.ShippingContact( + name = "", + line1 = "", + line2 = "", + postalCode = "", + city = "", + telephoneNumber = "", + mail = "", + deliveryInformation = "" + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/HowToRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/HowToRedeemScreen.kt new file mode 100644 index 00000000..2b2b2710 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/HowToRedeemScreen.kt @@ -0,0 +1,180 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.ui.components.TextFlatButton +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErezeptText +import de.gematik.ti.erp.app.utils.compose.ErezeptText.HeaderStyle +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class HowToRedeemScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + private val controller: OnlineRedeemGraphController +) : Screen() { + + @Composable + override fun Content() { + val prescriptions by controller.redeemableOrderState() + val listState = rememberLazyListState() + HowToRedeemScreenScaffold( + listState = listState, + onLocalClick = { navController.navigate(RedeemRoutes.RedeemLocal.path(taskId = "")) }, + onOnlineClick = { navController.navigateBasedOnPrescriptionSize(prescriptions.size) }, + onBack = { navController.popBackStack() } + ) + } + + companion object { + fun NavController.navigateBasedOnPrescriptionSize( + prescriptionSize: Int + ) = if (prescriptionSize == 1) { + navigate( + PharmacyRoutes.PharmacyStartScreenModal.path(taskId = "") + ) + } else { + navigate(RedeemRoutes.RedeemOnlinePreferences.route) + } + } +} + +@Composable +fun HowToRedeemScreenScaffold( + listState: LazyListState, + onLocalClick: () -> Unit, + onOnlineClick: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = "", + navigationMode = NavigationBarMode.Close, + listState = listState, + onBack = onBack + ) { + HowToRedeemScreenContent( + listState = listState, + onLocalClick = onLocalClick, + onOnlineClick = onOnlineClick + ) + } +} + +@Composable +fun HowToRedeemScreenContent( + listState: LazyListState, + onLocalClick: () -> Unit, + onOnlineClick: () -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Spacer(Modifier.height(SizeDefaults.fivefold)) + Image(painterResource(R.drawable.order_onb_pharmacist_blue), null) + Spacer(Modifier.height(SizeDefaults.tenfold)) + } + item { + ErezeptText.Title( + stringResource(R.string.order_onb_how_to_title), + style = HeaderStyle.H5, + textAlignment = TextAlignment.Center + ) + SpacerSmall() + } + item { + Text( + stringResource(R.string.order_onb_how_to_desc), + style = AppTheme.typography.subtitle2l, + textAlign = TextAlign.Center + ) + SpacerXXLarge() + } + item { + TextFlatButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.order_onb_how_to_local), + description = stringResource(R.string.order_onb_how_to_local_desc), + onClick = onLocalClick + ) + SpacerMedium() + } + item { + TextFlatButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.order_onb_how_to_online), + description = stringResource(R.string.order_onb_how_to_online_desc), + onClick = onOnlineClick + ) + Spacer(Modifier.navigationBarsPadding()) + } + } +} + +@LightDarkPreview +@Composable +fun HowToRedeemScaffoldScreenPreview() { + val listState = rememberLazyListState() + PreviewAppTheme { + HowToRedeemScreenScaffold( + listState = listState, + onLocalClick = {}, + onOnlineClick = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/LocalRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/LocalRedeemScreen.kt new file mode 100644 index 00000000..15ca0a68 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/LocalRedeemScreen.kt @@ -0,0 +1,345 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.redeem.presentation.rememberLocalRedeemScreenController +import de.gematik.ti.erp.app.redeem.ui.components.DataMatrixCodesWithSelfPayerWarning +import de.gematik.ti.erp.app.redeem.ui.preview.LocalRedeemPreview +import de.gematik.ti.erp.app.redeem.ui.preview.LocalRedeemPreviewParameter +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.forceBrightness +import de.gematik.ti.erp.app.utils.uistate.UiState + +@Requirement( + "A_20181-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Only the title of the prescription and the DMC consisting of Task-ID and Access-Code are displayed." +) +class LocalRedeemScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val taskId = navBackStackEntry.arguments?.getString("taskId") + val controller = rememberLocalRedeemScreenController(taskId ?: "") + + val codes by controller.dmCodes.collectAsStateWithLifecycle() + val showSingleCodes by controller.showSingleCodes.collectAsStateWithLifecycle() + var sharedWarningHeight by remember { mutableIntStateOf(0) } + + val activity = LocalActivity.current + activity.forceBrightness() + + val dialog = LocalDialog.current + val onClickEvent = ComposableEvent() + val onClose = { + navController.navigate(PrescriptionRoutes.PrescriptionsScreen.route) + } + + BackHandler { + if (codes.data?.any { it.containsScanned } == true) { + onClickEvent.trigger(Unit) + } else { + onClose() + } + } + + MarKScannedPrescriptionsRedeemedDialog(dialog, onClickEvent, onClose = onClose) { + controller.redeemPrescriptions() + } + + LocalRedeemScreenScaffold( + codes = codes, + showSingleCodes = showSingleCodes, + sharedWarningHeight = sharedWarningHeight, + onClickReady = { + if (codes.data?.any { it.containsScanned } == true) { + onClickEvent.trigger(Unit) + } else { + onClose() + } + }, + onSwitchSingleCodes = { + controller.switchSingleCode() + controller.getDmCodes() + }, + onRefreshCodes = { controller.refreshDmCodes() }, + onSharedWarningHeightUpdated = { sharedWarningHeight = it }, + onBack = { navController.popBackStack() } + ) + } +} + +@Composable +private fun LocalRedeemScreenScaffold( + codes: UiState>, + showSingleCodes: Boolean, + sharedWarningHeight: Int, + onClickReady: () -> Unit, + onSwitchSingleCodes: () -> Unit, + onRefreshCodes: () -> Unit, + onSharedWarningHeightUpdated: (Int) -> Unit, + onBack: () -> Unit +) { + val listState = rememberLazyListState() + val pagerState = rememberPagerState { codes.data?.size ?: 0 } + + AnimatedElevationScaffold( + listState = listState, + navigationMode = NavigationBarMode.Back, + topBarTitle = "", + onBack = onBack, + actions = { + TextButton( + onClick = onClickReady + ) { + Text(stringResource(R.string.local_redeem_done)) + } + } + ) { padding -> + + UiStateMachine( + state = codes, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onError = { + ErrorScreenComponent { onRefreshCodes() } + }, + onEmpty = { + ErrorScreenComponent { onRefreshCodes() } + }, + onContent = { codes -> + LocalRedeemScreenContent( + sharedWarningHeight = sharedWarningHeight, + padding = padding, + pagerState = pagerState, + codes = codes, + listState = listState, + showSingleCodes = showSingleCodes, + onSharedWarningHeightUpdated = onSharedWarningHeightUpdated + ) { + onSwitchSingleCodes() + } + } + ) + } +} + +@Composable +private fun LocalRedeemScreenContent( + sharedWarningHeight: Int, + padding: PaddingValues, + listState: LazyListState, + pagerState: PagerState, + codes: List, + showSingleCodes: Boolean, + onSharedWarningHeightUpdated: (Int) -> Unit, + onClick: () -> Unit +) { + LazyColumn( + Modifier + .padding(padding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + state = listState + ) { + item { + HorizontalPager( + state = pagerState + ) { page -> + DataMatrixCodesWithSelfPayerWarning( + dmCode = codes[page], + showSingleCodes = showSingleCodes, + sharedWarningHeight = sharedWarningHeight, + onSharedWarningHeightUpdated = onSharedWarningHeightUpdated + ) + } + } + item { + if (codes.size > 1 || (codes.size == 1 && codes.first().nrOfCodes > 1)) { + PageIndicator( + modifier = Modifier, + pagerState = pagerState + ) + Spacer(Modifier.fillMaxWidth()) + TertiaryButton( + modifier = Modifier + .padding(bottom = PaddingDefaults.XXLarge) + .navigationBarsPadding(), + onClick = onClick + ) { + if (showSingleCodes) { + Text(stringResource(R.string.local_redeem_grouped_codes)) + } else { + Text(stringResource(R.string.local_redeem_single_codes)) + } + } + } + } + } +} + +@Composable +private fun PageIndicator( + modifier: Modifier, + pagerState: PagerState +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(SizeDefaults.one)) + .background(AppTheme.colors.neutral100) + .padding(vertical = PaddingDefaults.Small, horizontal = PaddingDefaults.Medium) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + repeat(pagerState.pageCount) { + Dot(color = AppTheme.colors.neutral300) + } + } + + val fraction = pagerState.currentPage + pagerState.currentPageOffsetFraction + val offsetX = with(LocalDensity.current) { + val gap = PaddingDefaults.Small.roundToPx() + val size = SizeDefaults.one.roundToPx() + (fraction * (size + gap)).toDp() + } + Dot(modifier = Modifier.offset(x = offsetX), color = AppTheme.colors.primary500) + } +} + +@Composable +private fun Dot(modifier: Modifier = Modifier, color: Color) { + Box( + modifier = modifier + .clip(CircleShape) + .background(color) + .size(SizeDefaults.one) + ) +} + +@Composable +private fun MarKScannedPrescriptionsRedeemedDialog( + dialog: DialogScaffold, + onClickEvent: ComposableEvent, + onClose: () -> Unit, + markScannedAsRedeemed: () -> Unit +) { + onClickEvent.listen { + dialog.show { dialog -> + ErezeptAlertDialog( + title = stringResource(R.string.redeem_prescriptions_dialog_header), + bodyText = stringResource(R.string.redeem_prescriptions_dialog_info), + confirmText = stringResource(R.string.redeem_prescriptions_dialog_redeem), + dismissText = stringResource(R.string.redeem_prescriptions_dialog_cancel), + onDismissRequest = { + onClose() + dialog.dismiss() + }, + onConfirmRequest = { + markScannedAsRedeemed() + onClose() + dialog.dismiss() + } + ) + } + } +} + +@LightDarkPreview +@Composable +fun LocalRedeemScreenPreview( + @PreviewParameter(LocalRedeemPreviewParameter::class) previewData: LocalRedeemPreview +) { + PreviewAppTheme { + LocalRedeemScreenScaffold( + codes = previewData.dmCodes, + showSingleCodes = previewData.showSingleCodes, + sharedWarningHeight = 0, + onClickReady = {}, + onSwitchSingleCodes = {}, + onRefreshCodes = {}, + onSharedWarningHeightUpdated = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/OnlineRedeemPreferencesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/OnlineRedeemPreferencesScreen.kt new file mode 100644 index 00000000..9ff2e8d6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/OnlineRedeemPreferencesScreen.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewParameterProvider +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.ui.components.TextFlatButton +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class OnlineRedeemPreferencesScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val controller: OnlineRedeemGraphController +) : Screen() { + @Composable + override fun Content() { + val prescriptions by controller.redeemableOrderState() + val listState = rememberLazyListState() + AnimatedElevationScaffold( + topBarTitle = "", + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = { navController.popBackStack() }, + actions = {} + ) { + OnlineRedeemPreferencesScreenScaffold( + listState = listState, + prescriptions = prescriptions, + onNavigateToRedeemSelection = { + navController.navigate( + RedeemRoutes.RedeemPrescriptionSelection.path(isModal = false) + ) + }, + onNavigateToPharmacyStart = { + navController.navigate(PharmacyRoutes.PharmacyStartScreenModal.path(taskId = "")) + }, + onResetPrescriptionSelection = { controller.onResetPrescriptionSelection() } + ) + } + } +} + +@Composable +fun OnlineRedeemPreferencesScreenScaffold( + listState: LazyListState, + prescriptions: List, + onNavigateToRedeemSelection: () -> Unit, + onNavigateToPharmacyStart: () -> Unit, + onResetPrescriptionSelection: () -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Spacer(Modifier.height(SizeDefaults.fivefold)) + Image(painterResource(R.drawable.order_onb_blue_handoutmedicine), null) + Spacer(Modifier.height(SizeDefaults.tenfold)) + } + item { + Text( + stringResource(R.string.online_redeem_choose_rx_title), + style = AppTheme.typography.h5, + textAlign = TextAlign.Center + ) + SpacerSmall() + } + item { + Text( + pluralStringResource(R.plurals.online_redeem_rx_desc, prescriptions.size, prescriptions.size), + style = AppTheme.typography.subtitle2l, + textAlign = TextAlign.Center + ) + SpacerXXLarge() + } + item { + TextFlatButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.online_redeem_choose_rx), + onClick = onNavigateToRedeemSelection + ) + SpacerMedium() + } + item { + TextFlatButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.online_redeem_choose_all_rx), + onClick = { + onResetPrescriptionSelection() + onNavigateToPharmacyStart() + } + ) + Spacer(Modifier.navigationBarsPadding()) + } + } +} + +@LightDarkPreview +@Composable +fun OnlineRedeemPreferencesScreenPreview( + @PreviewParameter(OnlineRedeemPreferencesScreenPreviewParameterProvider::class) + prescriptions: List +) { + val listState = rememberLazyListState() + + PreviewAppTheme { + OnlineRedeemPreferencesScreenScaffold( + listState = listState, + prescriptions = prescriptions, + onNavigateToRedeemSelection = {}, + onNavigateToPharmacyStart = {}, + onResetPrescriptionSelection = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/PrescriptionSelectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/PrescriptionSelectionScreen.kt new file mode 100644 index 00000000..6462546f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/PrescriptionSelectionScreen.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.RadioButtonUnchecked +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescriptionId +import de.gematik.ti.erp.app.prescriptionIds +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonLarge +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar +import de.gematik.ti.erp.app.utils.extensions.dateTimeShortText + +class PrescriptionSelectionScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val controller: OnlineRedeemGraphController +) : Screen() { + @Composable + override fun Content() { + val snackbar = LocalSnackbar.current + val isOrderOverviewMode = navBackStackEntry.arguments?.getBoolean(RedeemRoutes.REDEEM_NAV_MODAL_BEHAVIOUR) ?: false + val selectedOrderState by controller.selectedOrderState() + val orderState by controller.redeemableOrderState() + val emptyOrdersCheckEvent = ComposableEvent() + + val listState = rememberLazyListState() + val snackbarText = stringResource(R.string.pharmacy_order_no_selected_prescriptions_desc) + + emptyOrdersCheckEvent.listen { + if (selectedOrderState.prescriptionOrders.isEmpty()) { + snackbar.show(snackbarText) + } + } + + val onBack: () -> Unit = { + when { + !isOrderOverviewMode -> { + controller.onResetPrescriptionSelection() + navController.popBackStack() + } + + isOrderOverviewMode && selectedOrderState.prescriptionOrders.isEmpty() -> { + snackbar.show(snackbarText) + } + + else -> { + navController.popBackStack() + } + } + } + + BackHandler { + onBack() + } + + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen), + topBarTitle = stringResource(R.string.pharmacy_order_select_prescriptions), + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = onBack + ) { + Column(Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .weight(1f) + .testTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .semantics { + prescriptionIds = orderState.map { it.taskId } + }, + state = listState + ) { + orderState.forEach { prescriptionOrder -> + item(key = "prescription-${prescriptionOrder.taskId}") { + PrescriptionItem( + modifier = Modifier, + prescription = prescriptionOrder, + checked = prescriptionOrder in selectedOrderState.prescriptionOrders, + onCheckedChange = { isChanged -> + controller.onPrescriptionSelectionChanged(prescriptionOrder, isChanged) + emptyOrdersCheckEvent.trigger() + } + ) + } + } + } + NextButton( + enabled = selectedOrderState.prescriptionOrders.isNotEmpty(), + onNext = { + when { + isOrderOverviewMode -> navController.popBackStack() + else -> navController.navigate( + PharmacyRoutes.PharmacyStartScreenModal.path(taskId = "") + ) + } + } + ) + } + } + } +} + +@Composable +private fun PrescriptionItem( + modifier: Modifier, + prescription: PharmacyUseCaseData.PrescriptionOrder, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .toggleable( + value = checked, + onValueChange = onCheckedChange, + role = Role.Checkbox + ) + .padding(PaddingDefaults.Medium) + .semantics { + selected = checked + prescriptionId = prescription.taskId + } + ) { + val prescriptionDateTime = remember(prescription) { dateTimeShortText(prescription.timestamp) } + Column(Modifier.weight(1f)) { + Text( + prescription.title + ?: "", + style = AppTheme.typography.body1 + ) + Text( + prescriptionDateTime, + style = AppTheme.typography.body2l + ) + } + SpacerMedium() + Box( + contentAlignment = Alignment.Center + ) { + if (checked) { + Icon( + Icons.Rounded.CheckCircle, + null, + tint = AppTheme.colors.primary600 + ) + } else { + Icon( + Icons.Rounded.RadioButtonUnchecked, + null, + tint = AppTheme.colors.neutral400 + ) + } + } + } +} + +@Composable +private fun NextButton( + enabled: Boolean, + onNext: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + elevation = 4.dp + ) { + Column(Modifier.navigationBarsPadding()) { + SpacerMedium() + PrimaryButtonLarge( + enabled = enabled, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton), + onClick = onNext + ) { + Text(stringResource(R.string.rx_selection_next)) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemEditShippingContactScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemEditShippingContactScreen.kt new file mode 100644 index 00000000..ff8215f4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemEditShippingContactScreen.kt @@ -0,0 +1,415 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.max +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.pharmacy.ui.components.addressSupplementInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.cityInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.deliveryInformationInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.mailInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.nameInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.phoneNumberInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.postalCodeInputField +import de.gematik.ti.erp.app.pharmacy.ui.components.streetAndNumberInputField +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyCity +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyLine1 +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyMail +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyName +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyPhoneNumber +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isEmptyPostalCode +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidCity +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidDeliveryInformation +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidLine1 +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidLine2 +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidMail +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidName +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidPhoneNumber +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isInvalidPostalCode +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isValid +import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.navigation.RedeemRouteBackStackEntryArguments +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.ui.preview.RedeemEditShippingPreviewParameter +import de.gematik.ti.erp.app.redeem.ui.preview.ShippingContactPreviewData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ComposableEvent.Companion.trigger +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.letNotNull + +data class ValidationResult( + val isEmpty: Boolean, + val isInvalid: Boolean +) + +@Requirement( + "O.Purp_2#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Contact information is collected when needed for redeeming." +) +class RedeemEditShippingContactScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnlineRedeemGraphController +) : Screen() { + + @Composable + override fun Content() { + val listState = rememberLazyListState() + val showDialogEvent = ComposableEvent() + + val activeProfile by graphController.activeProfile.collectAsStateWithLifecycle() + var isDirectRedeemEnabled = remember(activeProfile) { false } + + CheckAddressChangeDialog( + event = showDialogEvent, + dialogScaffold = LocalDialog.current, + onBack = { navController.popBackStack() } + ) + + navBackStackEntry.onReturnAction( + RedeemRoutes.RedeemEditShippingContactScreen + ) { + isDirectRedeemEnabled = activeProfile.data?.isDirectRedeemEnabled ?: false + } + + RedeemRouteBackStackEntryArguments(navBackStackEntry) + .getOrderOption()?.let { selectedOrderOption -> + + val orderState by graphController.selectedOrderState() + + var contact by remember(orderState.contact) { mutableStateOf(orderState.contact) } + + val shippingContactState = remember(orderState, contact) { + graphController.validateAndGetShippingContactState(contact, selectedOrderOption) + } + + letNotNull( + shippingContactState, + contact + ) { state, notNullContact -> + RedeemEditShippingContactScreenContent( + state = state, + notNullContact = notNullContact, + isDirectRedeemEnabled = isDirectRedeemEnabled, + listState = listState, + onContactChange = { contact = it }, + onSave = { + contact.let { + graphController.saveShippingContact(it) + navController.popBackStack() + } + }, + onShowDialog = { showDialogEvent.trigger() } + ) + } + } + } +} + +@Composable +fun RedeemEditShippingContactScreenContent( + state: ShippingContactState, + notNullContact: PharmacyUseCaseData.ShippingContact, + isDirectRedeemEnabled: Boolean, + listState: LazyListState, + onContactChange: (PharmacyUseCaseData.ShippingContact) -> Unit, + onSave: () -> Unit, + onShowDialog: () -> Unit +) { + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + bottomBar = { + @Requirement( + "O.Data_6#7", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Contact information is collected when needed for redeeming." + ) + SaveContactInformationBottomBar( + enabled = state.isValid(), + onClick = onSave + ) + }, + topBarTitle = stringResource(R.string.edit_shipping_contact_top_bar_title), + listState = listState, + onBack = { + if (state.isValid()) { + onSave() + } else { + onShowDialog() + } + } + ) { padding -> + + val imePadding = WindowInsets.ime.asPaddingValues() + val focusManager = LocalFocusManager.current + + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues( + top = PaddingDefaults.Medium + padding.calculateTopPadding(), + bottom = PaddingDefaults.Medium + max( + imePadding.calculateBottomPadding(), + padding.calculateBottomPadding() + ), + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + ) { + item { ContactNumberHeader() } + phoneNumberInputField( + listState = listState, + value = notNullContact.telephoneNumber, + validationResult = ValidationResult( + isEmpty = state.isEmptyPhoneNumber(), + isInvalid = state.isInvalidPhoneNumber() + ), + onValueChange = { phone -> + onContactChange(notNullContact.copy(telephoneNumber = phone.trim())) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + // we sent the mail currently only on direct redeem + if (isDirectRedeemEnabled) { + mailInputField( + listState = listState, + validationResult = ValidationResult( + isEmpty = state.isEmptyMail(), + isInvalid = state.isInvalidMail() + ), + value = notNullContact.mail, + onValueChange = { mail -> + onContactChange(notNullContact.copy(mail = mail.trim())) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + } + + item { DeliveryAddressHeader() } + + nameInputField( + listState = listState, + value = notNullContact.name, + onValueChange = { name -> + onContactChange(notNullContact.copy(name = name)) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + validationResult = ValidationResult( + isEmpty = state.isEmptyName(), + isInvalid = state.isInvalidName() + ) + ) + + streetAndNumberInputField( + listState = listState, + value = notNullContact.line1, + onValueChange = { line1 -> + onContactChange(notNullContact.copy(line1 = line1)) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + validationResult = ValidationResult( + isEmpty = state.isEmptyLine1(), + isInvalid = state.isInvalidLine1() + ) + ) + + addressSupplementInputField( + listState = listState, + value = notNullContact.line2, + validationResult = ValidationResult( + isEmpty = false, // optional, + isInvalid = state.isInvalidLine2() + ), + onValueChange = { line2 -> + onContactChange(notNullContact.copy(line2 = line2)) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + + postalCodeInputField( + listState = listState, + value = notNullContact.postalCode, + onValueChange = { postalCode -> + onContactChange(notNullContact.copy(postalCode = postalCode)) + }, + validationResult = ValidationResult( + isEmpty = state.isEmptyPostalCode(), + isInvalid = state.isInvalidPostalCode() + ), + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + + cityInputField( + listState = listState, + value = notNullContact.city, + onValueChange = { city -> + onContactChange(notNullContact.copy(city = city)) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + validationResult = ValidationResult( + isEmpty = state.isEmptyCity(), + isInvalid = state.isInvalidCity() + ) + ) + + deliveryInformationInputField( + listState = listState, + value = notNullContact.deliveryInformation, + validationResult = ValidationResult( + isEmpty = false, // optional, + isInvalid = state.isInvalidDeliveryInformation() + ), + onValueChange = { deliveryInformation -> + onContactChange(notNullContact.copy(deliveryInformation = deliveryInformation)) + }, + onSubmit = { focusManager.clearFocus() } + ) + } + } +} + +@Composable +fun SaveContactInformationBottomBar(enabled: Boolean, onClick: () -> Unit) { + BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { + Spacer(Modifier.weight(1f)) + Button( + onClick = onClick, + enabled = enabled + ) { + Text(stringResource(R.string.edit_shipping_contact_save)) + } + SpacerSmall() + } +} + +@Composable +private fun DeliveryAddressHeader() { + SpacerLarge() + Text( + stringResource(R.string.edit_shipping_contact_title_address), + style = AppTheme.typography.h6 + ) +} + +@Composable +fun CheckAddressChangeDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onBack: () -> Unit +) { + event.listen { + dialogScaffold.show { + ErezeptAlertDialog( + title = stringResource(R.string.edit_contact_back_alert_header), + bodyText = stringResource(R.string.edit_contact_back_alert_information), + dismissText = stringResource(R.string.edit_contact_back_alert_change), + confirmText = stringResource(R.string.edit_contact_back_alert_action), + onDismissRequest = { + it.dismiss() + }, + onConfirmRequest = { + it.dismiss() + onBack() + } + ) + } + } +} + +@Composable +fun ContactNumberHeader() { + Text( + stringResource(R.string.edit_shipping_contact_title_contact), + style = AppTheme.typography.h6 + ) +} + +@LightDarkPreview +@Composable +fun RedeemEditShippingScreenPreview( + @PreviewParameter(RedeemEditShippingPreviewParameter::class) shippingContactPreviewData: ShippingContactPreviewData +) { + PreviewAppTheme { + RedeemEditShippingContactScreenContent( + state = when { + shippingContactPreviewData.invalidShippingContactState != null -> { + ShippingContactState.InvalidShippingContactState( + errorList = shippingContactPreviewData.invalidShippingContactState.errorList + ) + } + shippingContactPreviewData.errorShippingContactState != null -> { + ShippingContactState.InvalidShippingContactState( + errorList = shippingContactPreviewData.errorShippingContactState.errorList + ) + } + else -> ShippingContactState.ValidShippingContactState.OK + }, + notNullContact = shippingContactPreviewData.shippingContact, + isDirectRedeemEnabled = true, + listState = rememberLazyListState(), + onContactChange = {}, + onSave = {}, + onShowDialog = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemOrderOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemOrderOverviewScreen.kt new file mode 100644 index 00000000..86650b33 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/screens/RedeemOrderOverviewScreen.kt @@ -0,0 +1,637 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.ui.screens + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHost +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.material.icons.outlined.Mail +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.authentication.ui.components.AuthenticationFailureDialog +import de.gematik.ti.erp.app.cardwall.navigation.CardWallRoutes +import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.loading.LoadingIndicator +import de.gematik.ti.erp.app.mainscreen.presentation.rememberAppController +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.navigateAndClearStack +import de.gematik.ti.erp.app.navigation.onReturnAction +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes +import de.gematik.ti.erp.app.pharmacy.ui.components.TopBarColor +import de.gematik.ti.erp.app.pharmacy.ui.components.VideoContent +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isContactInformationMissing +import de.gematik.ti.erp.app.pharmacy.usecase.GetShippingContactValidationUseCase.Companion.isValid +import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState +import de.gematik.ti.erp.app.pharmacy.usecase.ShippingContactState.ValidShippingContactState.OK +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.navigation.PrescriptionRoutes +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.redeem.navigation.RedeemRouteBackStackEntryArguments +import de.gematik.ti.erp.app.redeem.navigation.RedeemRoutes +import de.gematik.ti.erp.app.redeem.presentation.OnlineRedeemGraphController +import de.gematik.ti.erp.app.redeem.presentation.rememberOrderOverviewScreenController +import de.gematik.ti.erp.app.redeem.ui.components.RedeemButton +import de.gematik.ti.erp.app.redeem.ui.components.SelfPayerPrescriptionWarning +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerShortMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AcceptDialog +import de.gematik.ti.erp.app.utils.compose.ErrorScreenComponent +import de.gematik.ti.erp.app.utils.compose.NavigateBackButton +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.letNotNull +import kotlinx.coroutines.flow.collectLatest + +private const val OrderSuccessVideoAspectRatio = 1.69f + +class RedeemOrderOverviewScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + private val graphController: OnlineRedeemGraphController +) : Screen() { + @Composable + override fun Content() { + val intentHandler = LocalIntentHandler.current + navBackStackEntry.onReturnAction(RedeemRoutes.RedeemOrderOverviewScreen) { + // reload profile when user comes back to this screen to check for valid sso token + graphController.refreshActiveProfile() + } + val navigationArguments = RedeemRouteBackStackEntryArguments(navBackStackEntry) + + letNotNull( + navigationArguments.getPharmacy(), + navigationArguments.getOrderOption() + ) { pharmacy, orderOption -> + + val appController = rememberAppController() + val controller = rememberOrderOverviewScreenController() + + val activeProfile by graphController.activeProfile.collectAsStateWithLifecycle() + val isRedemptionAllowed by graphController.isRedemptionAllowed.collectAsStateWithLifecycle() + val isProfileRefreshing by controller.isProfileRefreshing.collectAsStateWithLifecycle() + + val taskId = navBackStackEntry.arguments?.getString(RedeemRoutes.REDEEM_NAV_TASK_ID) ?: "" + + LaunchedEffect(Unit) { + if (taskId.isNotEmpty()) { + graphController.deselectPrescriptions(taskId) + } + } + + val selectedOrderState by graphController.selectedOrderState() + + val shippingContactState = remember(selectedOrderState, orderOption) { + graphController.validateAndGetShippingContactState(selectedOrderState.contact, orderOption) + } + + var isLoadingIndicatorShown by remember { mutableStateOf(false) } + + fun hideLoadingIndicator() { + isLoadingIndicatorShown = false + } + + fun showLoadingIndicator() { + isLoadingIndicatorShown = true + } + + LaunchedEffect(isProfileRefreshing) { + when { + isProfileRefreshing -> showLoadingIndicator() + else -> hideLoadingIndicator() + } + } + + LaunchedEffect(Unit) { + intentHandler.gidSuccessfulIntent.collectLatest { + graphController.refreshActiveProfile() + } + } + + AuthenticationFailureDialog( + event = controller.showAuthenticationErrorDialog, + dialogScaffold = dialog + ) + + Box { + UiStateMachine( + state = activeProfile, + onLoading = { + Center { + CircularProgressIndicator() + } + } + ) { profile -> + + with(controller) { + onBiometricAuthenticationSuccessEvent.listen { + graphController.refreshActiveProfile() + } + + showCardWallEvent.listen { id -> + navController.navigate(CardWallRoutes.CardWallIntroScreen.path(id)) + } + showCardWallWithFilledCanEvent.listen { cardWallData -> + navController.navigate( + CardWallRoutes.CardWallPinScreen.path( + profileIdentifier = cardWallData.profileId, + can = cardWallData.can + ) + ) + } + showGidEvent.listen { gidData -> + navController.navigate( + CardWallRoutes.CardWallIntroScreen.pathWithGid( + profileIdentifier = gidData.profileId, + gidEventData = gidData + ) + ) + } + } + + RedeemOrderOverviewScreenContent( + profile = profile, + selectedOrderState = selectedOrderState, + selectedOrderOption = orderOption, + shippingContactState = shippingContactState, + selectedPharmacy = pharmacy, + isRedemptionPossible = isRedemptionAllowed, + onClickContacts = { + hideLoadingIndicator() + navController.navigate( + RedeemRoutes.RedeemEditShippingContactScreen.path(orderOption) + ) + }, + onSelectPrescriptions = { + hideLoadingIndicator() + navController.navigate(RedeemRoutes.RedeemPrescriptionSelection.path(isModal = true)) + }, + onBack = { + hideLoadingIndicator() + graphController.onResetPrescriptionSelection() + navController.popBackStack() + }, + onChangePharmacy = { + hideLoadingIndicator() + navController.navigate( + PharmacyRoutes.PharmacyStartScreenModal.path(taskId = "") + ) + }, + onNotRedeemable = { + controller.chooseAuthenticationMethod(profile.id) + }, + onProcessStarted = { + showLoadingIndicator() + }, + onProcessEnded = { + hideLoadingIndicator() + }, + onFinish = { orderHasError -> + hideLoadingIndicator() + appController.onOrdered(hasError = orderHasError) + graphController.onResetPrescriptionSelection() + navController.navigateAndClearStack(route = PrescriptionRoutes.PrescriptionsScreen.route) + } + ) + } + + if (isLoadingIndicatorShown) { + LoadingIndicator() + } + } + } ?: run { + // in case of missing pharmacy or order option we show an error screen asking the user to go back and start the process + Center { + ErrorScreenComponent( + onClickRetry = navController::navigateUp + ) + } + } + } +} + +@Composable +fun RedeemOrderOverviewScreenContent( + profile: ProfilesUseCaseData.Profile, + selectedOrderState: PharmacyUseCaseData.OrderState, + selectedOrderOption: PharmacyScreenData.OrderOption?, + shippingContactState: ShippingContactState?, + selectedPharmacy: PharmacyUseCaseData.Pharmacy?, + isRedemptionPossible: Boolean, + onFinish: (Boolean) -> Unit, + onNotRedeemable: () -> Unit, + onClickContacts: () -> Unit, + onSelectPrescriptions: () -> Unit, + onChangePharmacy: () -> Unit, + onProcessStarted: () -> Unit, + onProcessEnded: () -> Unit, + onBack: () -> Unit +) { + val listState = rememberLazyListState() + val videoHeightPx = remember { mutableFloatStateOf(0f) } + val scaffoldState = rememberScaffoldState() + + Scaffold( + modifier = Modifier.testTag(TestTag.PharmacySearch.OrderSummary.Screen), + scaffoldState = scaffoldState, + snackbarHost = { SnackbarHost(it, modifier = Modifier.systemBarsPadding()) }, + topBar = { + // no top bar + } + ) { padding -> + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .padding(padding), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) + ) { + item { + val shape = RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp) + Box { + VideoContent( + Modifier + .onPlaced { + videoHeightPx.floatValue = it.size.height.toFloat() + } + .clip(shape) + .background(TopBarColor) + .statusBarsPadding() + .fillMaxWidth(), + source = when (selectedOrderOption) { + PharmacyScreenData.OrderOption.PickupService -> R.raw.animation_local + PharmacyScreenData.OrderOption.CourierDelivery -> R.raw.animation_courier + PharmacyScreenData.OrderOption.MailDelivery -> R.raw.animation_mail + else -> { + // show default animation until the order option is not null + R.raw.animation_local + } + }, + aspectRatioOverwrite = OrderSuccessVideoAspectRatio + ) + NavigateBackButton(onClick = onBack) + } + } + item { + SpacerMedium() + Text( + stringResource(R.string.pharmacy_order_title), + textAlign = TextAlign.Center, + style = AppTheme.typography.h5, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + ) + } + item { + Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { + Text(stringResource(R.string.pharmacy_order_receiver), style = AppTheme.typography.h6) + SpacerMedium() + shippingContactState?.let { shippingContact -> + ContactSelectionButton( + contact = selectedOrderState.contact, + shippingContactState = shippingContact, + onClick = onClickContacts + ) + } + } + } + item { + Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { + Text(stringResource(R.string.pharmacy_order_prescriptions), style = AppTheme.typography.h6) + SpacerMedium() + selectedOrderState.prescriptionOrders.takeIf { it.isNotEmpty() }?.let { prescriptions -> + PrescriptionSelectionButton( + prescriptions = prescriptions, + onClick = onSelectPrescriptions + ) + } + } + } + item { + Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { + Text(stringResource(R.string.pharmacy_order_pharmacy), style = AppTheme.typography.h6) + SpacerMedium() + letNotNull(selectedPharmacy, selectedOrderOption) { pharmacy, orderOption -> + PharmacySelectionButton( + selectedPharmacy = pharmacy, + selectedOrderOption = orderOption, + onClick = onChangePharmacy + ) + } + SpacerMedium() + } + } + + val selectedSelfPayerPrescriptionNames = selectedOrderState.prescriptionOrders + .filter { it.taskId in selectedOrderState.selfPayerPrescriptionIds } + .mapNotNull { it.title } + + if (selectedSelfPayerPrescriptionNames.isNotEmpty()) { + item { + SelfPayerPrescriptionWarning( + selectedSelfPayerPrescriptionNames + ) + } + } + item { + letNotNull( + profile, + selectedPharmacy, + selectedOrderOption + ) { activeProfile, pharmacy, orderOption -> + RedeemButton( + profile = activeProfile, + selectedPharmacy = pharmacy, + selectedOrderOption = orderOption, + order = selectedOrderState, + shippingContactCompleted = shippingContactState == OK, + isRedemptionPossible = isRedemptionPossible, + onNotRedeemable = onNotRedeemable, + onProcessStarted = onProcessStarted, + onProcessEnded = onProcessEnded, + onFinish = onFinish + ) + } + } + } + } +} + +@Composable +fun PrescriptionRedeemAlertDialog( + title: String, + description: String, + onDismiss: () -> Unit +) { + AcceptDialog( + header = title, + info = description, + onClickAccept = { + onDismiss() + }, + acceptText = stringResource(R.string.pharmacy_search_apovz_call_failed_accept) + ) +} + +@Composable +private fun ContactSelectionButton( + contact: PharmacyUseCaseData.ShippingContact, + onClick: () -> Unit, + shippingContactState: ShippingContactState +) { + FlatButton( + onClick = onClick + ) { + if (contact.isEmpty()) { + Text( + stringResource(R.string.pharmacy_order_add_contacts), + style = AppTheme.typography.subtitle1, + color = AppTheme.colors.primary600 + ) + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Column { + if (contact.name.isNotBlank()) { + Text(contact.name, style = AppTheme.typography.subtitle1) + } + contact.address().forEach { + Text(it, style = AppTheme.typography.body1l) + } + } + if (contact.other().isNotEmpty()) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + if (contact.telephoneNumber.isNotBlank()) { + SmallChip(Icons.Outlined.Phone, contact.telephoneNumber) + } + if (contact.mail.isNotBlank()) { + SmallChip(Icons.Outlined.Mail, contact.mail) + } + } + } + if (contact.deliveryInformation.isNotBlank()) { + Text(contact.deliveryInformation, style = AppTheme.typography.body1l) + } + } + if (!shippingContactState.isValid()) { + val text = if (shippingContactState.isContactInformationMissing()) { + stringResource(R.string.pharmacy_order_further_contact_information_required) + } else { + stringResource(R.string.pharmacy_order_contact_information_invalid) + } + SpacerSmall() + Surface(shape = RoundedCornerShape(8.dp), color = AppTheme.colors.red100) { + Text( + text, + color = AppTheme.colors.red900, + style = AppTheme.typography.subtitle2, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + } + SpacerMedium() + Text( + stringResource(R.string.pharmacy_order_change_contacts), + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600 + ) + } + } + } +} + +@Composable +private fun PrescriptionSelectionButton( + prescriptions: List, + onClick: () -> Unit +) { + val (title, desc) = when (prescriptions.size) { + 1 -> Pair(prescriptions.first().title ?: "", null) + else -> Pair( + stringResource(R.string.pharmacy_order_nr_of_prescriptions, prescriptions.size), + prescriptions.joinToString { it.title ?: "" } + ) + } + + FlatButton( + modifier = Modifier.testTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton), + onClick = onClick + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text(title, style = AppTheme.typography.subtitle1) + desc?.let { + SpacerTiny() + Text(desc, style = AppTheme.typography.body2l, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } + SpacerMedium() + Icon(Icons.AutoMirrored.Rounded.KeyboardArrowRight, null) + } + } +} + +@Composable +private fun SmallChip( + icon: ImageVector, + text: String +) = + Surface(shape = RoundedCornerShape(SizeDefaults.one), color = AppTheme.colors.neutral100) { + Row( + Modifier.padding(horizontal = PaddingDefaults.Small, vertical = SizeDefaults.quarter), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(icon, null, tint = AppTheme.colors.neutral500) + SpacerSmall() + Text( + text, + style = AppTheme.typography.body1 + ) + } + } + +@Composable +private fun PharmacySelectionButton( + selectedPharmacy: PharmacyUseCaseData.Pharmacy, + selectedOrderOption: PharmacyScreenData.OrderOption, + onClick: () -> Unit +) { + FlatButton( + onClick = onClick + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + Text( + selectedPharmacy.name, + style = AppTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerTiny() + Text( + selectedPharmacy.singleLineAddress(), + style = AppTheme.typography.body2l, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + SpacerShortMedium() + ServiceOption( + option = selectedOrderOption + ) + } + SpacerMedium() + Text( + stringResource(R.string.pharmacy_order_change_order), + style = AppTheme.typography.subtitle2, + color = AppTheme.colors.primary600 + ) + } + } +} + +@Composable +private fun ServiceOption( + option: PharmacyScreenData.OrderOption +) { + val text = when (option) { + PharmacyScreenData.OrderOption.PickupService -> stringResource(R.string.pharmacy_order_collect) + PharmacyScreenData.OrderOption.CourierDelivery -> stringResource(R.string.pharmacy_order_delivery) + PharmacyScreenData.OrderOption.MailDelivery -> stringResource(R.string.pharmacy_order_mail) + } + val shape = RoundedCornerShape(8.dp) + Box( + Modifier + .background(AppTheme.colors.green200, shape) + .padding( + horizontal = PaddingDefaults.ShortMedium, + vertical = PaddingDefaults.ShortMedium / 2 + ) + ) { + Text(text, style = AppTheme.typography.subtitle2, color = AppTheme.colors.green900) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun FlatButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + content: @Composable () -> Unit +) = + Surface( + modifier = modifier.fillMaxWidth(), + onClick = onClick, + shape = RoundedCornerShape(16.dp), + color = AppTheme.colors.neutral025, + border = BorderStroke(1.dp, AppTheme.colors.neutral300), + elevation = 0.dp + ) { + Box(Modifier.padding(PaddingDefaults.Medium)) { + content() + } + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetDMCodesForLocalRedeemUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetDMCodesForLocalRedeemUseCase.kt new file mode 100644 index 00000000..4c61ce80 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetDMCodesForLocalRedeemUseCase.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.model.DMCode +import de.gematik.ti.erp.app.utils.createDMPayload +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class GetDMCodesForLocalRedeemUseCase { + companion object { + const val MaxTasks = 3 + const val Four = 4 + } + + operator fun invoke( + prescriptionOrders: Flow>, + showSingleCodes: Flow + ): Flow> = combine(prescriptionOrders, showSingleCodes) { orders, showSingleCode -> + + val maxTasks = + when { + showSingleCode -> 1 + orders.size == Four -> 2 // split to 2x2 codes + else -> MaxTasks + } + + orders + .map { prescription -> + prescription to "Task/${prescription.taskId}/\$accept?ac=${prescription.accessCode}" + } + .windowed(maxTasks, maxTasks, partialWindows = true) + .map { codes -> + val prescriptions = codes.map { it.first } + val urls = codes.map { it.second } + val json = createDMPayload(urls) + DMCode( + payload = json, + nrOfCodes = urls.size, + name = prescriptions.mapNotNull { it.title }.joinToString { it }, + selfPayerPrescriptionNames = prescriptions.filter { it.isSelfPayerPrescription }.map { it.title }, + containsScanned = prescriptions.any { it.isScanned } + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetRedeemableTasksForDmCodesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetRedeemableTasksForDmCodesUseCase.kt new file mode 100644 index 00000000..8264326b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/GetRedeemableTasksForDmCodesUseCase.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.pharmacy.mapper.toOrder +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +class GetRedeemableTasksForDmCodesUseCase( + private val prescriptionRepository: PrescriptionRepository, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke( + profileId: ProfileIdentifier + + ): Flow> = + combine( + prescriptionRepository.syncedTasks(profileId).mapNotNull { tasks -> + tasks.filter { + it.redeemState().isRedeemable() + }.sortedByDescending { it.authoredOn } + .map { + it.toOrder() + } + }, + prescriptionRepository.scannedTasks(profileId).mapNotNull { tasks -> + tasks.filter { + it.isRedeemable() + }.sortedByDescending { it.scannedOn } + .map { + it.toOrder() + } + } + ) { syncedTasks, scannedTasks -> + val prescriptionOrderList = mutableListOf() + prescriptionOrderList.addAll(scannedTasks) + prescriptionOrderList.addAll(syncedTasks) + prescriptionOrderList + }.flowOn(dispatchers) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/HasRedeemableTasksUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/HasRedeemableTasksUseCase.kt new file mode 100644 index 00000000..aa43bc52 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/HasRedeemableTasksUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class HasRedeemableTasksUseCase( + private val prescriptionRepository: PrescriptionRepository, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke( + profileId: ProfileIdentifier + ): Flow = + combine( + prescriptionRepository.syncedTasks(profileId).map { tasks -> + tasks.filter { + it.redeemState().isRedeemable() + } + }, + prescriptionRepository.scannedTasks(profileId).map { tasks -> + tasks.filter { + it.isRedeemable() + } + } + ) { syncedTasks, scannedTasks -> + syncedTasks.isNotEmpty() || scannedTasks.isNotEmpty() + }.flowOn(dispatchers) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnDirectUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnDirectUseCase.kt new file mode 100644 index 00000000..5ba7b280 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnDirectUseCase.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.BCProvider +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.api.httpErrorState +import de.gematik.ti.erp.app.fhir.model.DirectCommunicationMessage +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.pharmacy.buildRecipientInfo +import de.gematik.ti.erp.app.pharmacy.filterByRSAPublicKey +import de.gematik.ti.erp.app.pharmacy.mapper.toPharmacyContact +import de.gematik.ti.erp.app.pharmacy.mapper.toRedeemOption +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.model.RedeemedPrescriptionState +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.DERSet +import org.bouncycastle.asn1.cms.Attribute +import org.bouncycastle.asn1.cms.AttributeTable +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cms.CMSAlgorithm +import org.bouncycastle.cms.CMSAuthEnvelopedDataGenerator +import org.bouncycastle.cms.CMSProcessableByteArray +import org.bouncycastle.cms.SimpleAttributeTableGenerator +import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder +import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator +import org.bouncycastle.operator.OutputAEADEncryptor +import org.bouncycastle.operator.jcajce.JceAsymmetricKeyWrapper +import org.bouncycastle.util.encoders.Base64 +import java.security.spec.MGF1ParameterSpec +import java.util.UUID +import javax.crypto.spec.OAEPParameterSpec +import javax.crypto.spec.PSource + +private val json = Json { + encodeDefaults = true + prettyPrint = false +} +private const val OidRecipientMail = "1.2.276.0.76.4.173" // komle-recipient-emails + +/** + * 1. Inform the UI on the process start + * 2. Load the pharmacy certificates using the pharmacy id. This is required to encrypt the message + * 3. Create the communication message in the FHIR Json format + * 4. Encrypt the message using the pharmacy certificates and obtain the encrypted byte array + * 5. Send the encrypted message to the pharmacy with the prescription information in an async manner + * 6. Collect the results on the responses for all the prescriptions + * 7. Save the communication to the database and the pharmacy as often used + * 8. Inform the UI on the process end + * 9. Combine the results together in [RedeemedPrescriptionState.OrderCompleted] and return the flow + */ +class RedeemPrescriptionsOnDirectUseCase( + private val communicationRepository: CommunicationRepository, + private val pharmacyRepository: PharmacyRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + orderId: UUID, + redeemOption: PharmacyScreenData.OrderOption, + prescriptionOrderInfos: List, + contact: PharmacyUseCaseData.ShippingContact, + pharmacy: PharmacyUseCaseData.Pharmacy, + onProcessStart: () -> Unit, + onProcessEnd: () -> Unit + ): Flow = + flow { + withContext(dispatcher) { + onProcessStart() + + val certificates = loadCertificates(pharmacy.id).getOrThrow() + + prescriptionOrderInfos + .map { prescriptionOrderInfo -> + async { + prescriptionOrderInfo to pharmacyRepository.redeemPrescriptionDirectly( + url = redeemOption.toPharmacyContact(pharmacy), + message = convertJsonMessageToByteArray( + message = createCommunication( + orderId = orderId, + prescription = prescriptionOrderInfo, + contact = contact, + redeemOption = redeemOption + ), + recipientCertificates = certificates + ), + pharmacyTelematikId = pharmacy.telematikId, + transactionId = orderId.toString() + ) + } + } + .awaitAll() + .toMap() + .mapValues { (prescriptionOrderInfo, redeemResult) -> + redeemResult.fold( + onSuccess = { + onProcessEnd() + Napier.i { "Prescription (direct) ${prescriptionOrderInfo.title} redeemed successfully" } + try { + // save the communication to the database + communicationRepository.saveLocalCommunication( + taskId = prescriptionOrderInfo.taskId, + pharmacyId = pharmacy.id, + transactionId = orderId.toString() + ) + + // save the pharmacy as often used when the prescription was redeemed successfully + launch { pharmacyRepository.markPharmacyAsOftenUsed(pharmacy) } + + RedeemedPrescriptionState.Success + } catch (e: Throwable) { + RedeemedPrescriptionState.Error( + errorState = HttpErrorState.ErrorWithCause("Error on local save ${e.message}") + ) + } + }, + onFailure = { error -> + Napier.e { "Error on prescription (direct) ${prescriptionOrderInfo.title} redemption ${error.stackTraceToString()}" } + onProcessEnd() + when (error) { + is ApiCallException -> RedeemedPrescriptionState.Error( + errorState = error.response.httpErrorState() + ) + + else -> RedeemedPrescriptionState.Error( + errorState = HttpErrorState.ErrorWithCause("Error on local save ${error.message}") + ) + } + } + ) + } + }.also { emit(it) } + } + .map { RedeemedPrescriptionState.OrderCompleted(orderId = orderId.toString(), results = it) } + .cancellable() + + private suspend fun loadCertificates(pharmacyId: String): Result> = + pharmacyRepository.searchBinaryCerts(locationId = pharmacyId).mapCatching { list -> + list.map { base64Cert -> + X509CertificateHolder(Base64.decode(base64Cert)) + } + } + + private fun createCommunication( + orderId: UUID, + prescription: PharmacyUseCaseData.PrescriptionOrder, + contact: PharmacyUseCaseData.ShippingContact, + redeemOption: PharmacyScreenData.OrderOption + ): String { + val communication = DirectCommunicationMessage( + version = 2, + supplyOptionsType = redeemOption.toRedeemOption().type, + name = contact.name, + address = listOf(contact.line1, contact.line2, contact.postalCode, contact.city), + phone = contact.telephoneNumber, + hint = contact.deliveryInformation, + text = "", + mail = contact.mail, + transactionID = orderId.toString(), + taskID = prescription.taskId, + accessCode = prescription.accessCode + ) + + val jsonString = json.encodeToString(communication) + return jsonString + } + + @Requirement( + "A_22778-01#3", + "A_22779-01#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Build and encrypt direct redeem message with pharmacy`s certificate" + ) + fun convertJsonMessageToByteArray( + message: String, + recipientCertificates: List + ): ByteArray { + require(recipientCertificates.isNotEmpty()) { "No recipients specified!" } + + val msg = CMSProcessableByteArray(message.toByteArray()) + val edGen = CMSAuthEnvelopedDataGenerator() + val info = buildRecipientInfo(recipientCertificates) + + edGen.setUnauthenticatedAttributeGenerator( + SimpleAttributeTableGenerator( + AttributeTable( + Attribute( + ASN1ObjectIdentifier(OidRecipientMail), + DERSet(info) + ) + ) + ) + ) + + val jcaConverter = JcaX509CertificateConverter().apply { + setProvider(BCProvider) + } + @Requirement( + "GS-A_4389#3", + "GS-A_4390#1", + sourceSpecification = "gemSpec_Krypt", + rationale = "Build and encrypt direct redeem message with pharmacy`s certificate" + ) + recipientCertificates + .filterByRSAPublicKey() + .forEach { recipientCert -> + val jcaCert = jcaConverter.getCertificate(recipientCert) + + edGen.addRecipientInfoGenerator( + JceKeyTransRecipientInfoGenerator( + jcaCert, + JceAsymmetricKeyWrapper( + OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT), + jcaCert.publicKey + ) + ).setProvider(BCProvider) + ) + } + + val contentEncryptor = JceCMSContentEncryptorBuilder(CMSAlgorithm.AES256_GCM) + .setProvider(BCProvider) + .build() + + val ed = edGen.generate(msg, contentEncryptor as OutputAEADEncryptor) + + return ed.toASN1Structure().encoded + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnLoggedInUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnLoggedInUseCase.kt new file mode 100644 index 00000000..a0696751 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemPrescriptionsOnLoggedInUseCase.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.api.httpErrorState +import de.gematik.ti.erp.app.fhir.model.CommunicationPayload +import de.gematik.ti.erp.app.pharmacy.mapper.toRedeemOption +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.redeem.model.COMMUNICATION_PROFILE_1_2 +import de.gematik.ti.erp.app.redeem.model.Communication +import de.gematik.ti.erp.app.redeem.model.Identifier +import de.gematik.ti.erp.app.redeem.model.Meta +import de.gematik.ti.erp.app.redeem.model.ORDER_ID_IDENTIFIER +import de.gematik.ti.erp.app.redeem.model.Payload +import de.gematik.ti.erp.app.redeem.model.RECIPIENT_IDENTIFIER +import de.gematik.ti.erp.app.redeem.model.Recipient +import de.gematik.ti.erp.app.redeem.model.RecipientIdentifier +import de.gematik.ti.erp.app.redeem.model.RedeemedPrescriptionState +import de.gematik.ti.erp.app.redeem.model.Reference +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import java.util.UUID + +private val json = Json { + encodeDefaults = true + prettyPrint = false +} + +/** + * 1. Inform the UI on the process start + * 2. Create the communication message in the FHIR Json format + * 3. Send the encrypted message to the pharmacy with the prescription information in an async manner + * 4. Collect the results on the responses for all the prescriptions + * 5. Inform the UI on the process end + * 6. Combine the results together in [RedeemedPrescriptionState.OrderCompleted] and return the flow + */ +class RedeemPrescriptionsOnLoggedInUseCase( + private val prescriptionRepository: PrescriptionRepository, + private val pharmacyRepository: PharmacyRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + profileId: ProfileIdentifier, + redeemOption: PharmacyScreenData.OrderOption, + orderId: UUID, + prescriptionOrderInfos: List, + contact: PharmacyUseCaseData.ShippingContact, + pharmacy: PharmacyUseCaseData.Pharmacy, + onProcessStart: () -> Unit, + onProcessEnd: () -> Unit + ): Flow = + flow { + withContext(dispatcher) { + onProcessStart() + prescriptionOrderInfos + .map { prescriptionOrderInfo -> + async { + // create a communication dispense request + val communicationDispenseRequestJson = createCommunicationJson( + orderId = orderId.toString(), + taskId = prescriptionOrderInfo.taskId, + accessCode = prescriptionOrderInfo.accessCode, + recipientTID = pharmacy.telematikId, + payloadContent = CommunicationPayload( + supplyOptionsType = redeemOption.toRedeemOption().type, + name = contact.name, + address = listOf(contact.line1, contact.line2, contact.postalCode, contact.city), + phone = contact.telephoneNumber, + hint = contact.deliveryInformation + ) + ) + + // save the pharmacy as often used when the prescription was redeemed successfully + launch { pharmacyRepository.markPharmacyAsOftenUsed(pharmacy) } + + // redeem the prescription + prescriptionOrderInfo to prescriptionRepository.redeemPrescription( + profileId = profileId, + communication = communicationDispenseRequestJson, + accessCode = prescriptionOrderInfo.accessCode + ) + } + } + .awaitAll() + .toMap() + .mapValues { (prescriptionOrderInfo, redeemResult) -> + redeemResult.fold( + onSuccess = { jsonElement -> + Napier.i { "Prescription redeemed successfully (${prescriptionOrderInfo.title} ): $jsonElement" } + onProcessEnd() + RedeemedPrescriptionState.Success + }, + onFailure = { error -> + Napier.e { "Error on prescription redemption (${prescriptionOrderInfo.title}) ${error.stackTraceToString()}" } + onProcessEnd() + when (error) { + is ApiCallException -> RedeemedPrescriptionState.Error( + errorState = error.response.httpErrorState() + ) + + else -> RedeemedPrescriptionState.Error( + errorState = HttpErrorState.ErrorWithCause(error.message ?: "Unknown error") + ) + } + } + ) + } + }.also { emit(it) } + } + .map { RedeemedPrescriptionState.OrderCompleted(orderId = orderId.toString(), results = it) } + .cancellable() + + private fun createCommunicationJson( + orderId: String, + taskId: String, + accessCode: String, + recipientTID: String, + payloadContent: CommunicationPayload + ): JsonElement { + val communication = Communication( + meta = Meta( + profile = listOf(COMMUNICATION_PROFILE_1_2) + ), + identifier = listOf( + Identifier( + system = ORDER_ID_IDENTIFIER, + value = orderId + ) + ), + basedOn = listOf( + Reference( + reference = "Task/$taskId/\$accept?ac=$accessCode" + ) + ), + recipient = listOf( + Recipient( + identifier = RecipientIdentifier( + system = RECIPIENT_IDENTIFIER, + value = recipientTID + ) + ) + ), + payload = listOf( + Payload( + contentString = json.encodeToString(payloadContent) + ) + ) + ) + + val jsonString = json.encodeToString(Communication.serializer(), communication) + Napier.d { "Communication dispense request created for order $orderId: $jsonString" } + + return json.parseToJsonElement(jsonString) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemScannedTasksUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemScannedTasksUseCase.kt new file mode 100644 index 00000000..1414965a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemScannedTasksUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.usecase + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class RedeemScannedTasksUseCase( + private val repository: PrescriptionRepository, + val dispatcher: CoroutineDispatcher = Dispatchers.IO + +) { + suspend operator fun invoke(taskIds: List) { + withContext(dispatcher) { + repository.redeemScannedTasks(taskIds) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt deleted file mode 100644 index eeb979af..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.redeem.usecase - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.datetime.Clock - -class RedeemUseCase( - private val prescriptionRepository: PrescriptionRepository, - private val dispatchers: DispatchProvider -) { - fun hasRedeemablePrescriptions( - profileId: ProfileIdentifier - ): Flow = - combine( - prescriptionRepository.syncedTasks(profileId).map { tasks -> - tasks.filter { - it.redeemState().isRedeemable() - } - }, - prescriptionRepository.scannedTasks(profileId).map { tasks -> - tasks.filter { - it.isRedeemable() - } - } - ) { syncedTasks, scannedTasks -> - syncedTasks.isNotEmpty() || scannedTasks.isNotEmpty() - }.flowOn(dispatchers.io) - - suspend fun redeemScannedTasks(taskIds: List) { - val redeemedOn = Clock.System.now() - taskIds.forEach { taskId -> - prescriptionRepository.updateRedeemedOn(taskId, redeemedOn) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt index f0cb43ff..0da82911 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt @@ -1,39 +1,44 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings import de.gematik.ti.erp.app.settings.repository.CardWallRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import de.gematik.ti.erp.app.settings.repository.SettingsRepository import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase -import de.gematik.ti.erp.app.settings.usecase.GetAuthenticationModeUseCase +import de.gematik.ti.erp.app.settings.usecase.DefaultXmlResourceParserWrapper +import de.gematik.ti.erp.app.settings.usecase.DisableDeviceSecurityUseCase +import de.gematik.ti.erp.app.settings.usecase.GetAuthenticationUseCase import de.gematik.ti.erp.app.settings.usecase.GetCanStartToolTipsUseCase import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase -import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase +import de.gematik.ti.erp.app.settings.usecase.GetOrganDonationRegisterHostsUseCase import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase +import de.gematik.ti.erp.app.settings.usecase.GetSupportedLanguagesFromXmlUseCase import de.gematik.ti.erp.app.settings.usecase.GetZoomStateUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveDeviceSecurityUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingDataUseCase -import de.gematik.ti.erp.app.settings.usecase.SavePasswordUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveToolTippsShownUseCase +import de.gematik.ti.erp.app.settings.usecase.EnableDeviceSecurityUseCase +import de.gematik.ti.erp.app.settings.usecase.ResetPasswordUseCase +import de.gematik.ti.erp.app.settings.usecase.SetPasswordUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveToolTipsShownUseCase import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase import de.gematik.ti.erp.app.settings.usecase.SaveZoomPreferenceUseCase +import de.gematik.ti.erp.app.settings.usecase.XmlResourceParserWrapper import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance @@ -41,20 +46,35 @@ import org.kodein.di.instance const val ApplicationPreferencesTag = "ApplicationPreferences" val settingsModule = DI.Module("settingsModule") { - bindProvider { CardWallRepository(prefs = instance(ApplicationPreferencesTag)) } - bindProvider { SettingsRepository(instance(), instance()) } bindProvider { GetScreenShotsAllowedUseCase(instance()) } bindProvider { AllowScreenshotsUseCase(instance()) } - bindProvider { GetOnboardingSucceededUseCase(instance()) } - bindProvider { SaveOnboardingDataUseCase(instance()) } bindProvider { GetMLKitAcceptedUseCase(instance()) } bindProvider { GetCanStartToolTipsUseCase(instance()) } - bindProvider { SaveToolTippsShownUseCase(instance()) } - bindProvider { SavePasswordUseCase(instance()) } + bindProvider { SaveToolTipsShownUseCase(instance()) } + bindProvider { SetPasswordUseCase(instance()) } bindProvider { GetShowWelcomeDrawerUseCase(instance()) } bindProvider { SaveWelcomeDrawerShownUseCase(instance()) } - bindProvider { GetAuthenticationModeUseCase(instance()) } + bindProvider { GetAuthenticationUseCase(instance()) } bindProvider { GetZoomStateUseCase(instance()) } - bindProvider { SaveDeviceSecurityUseCase(instance()) } + bindProvider { EnableDeviceSecurityUseCase(instance()) } + bindProvider { DisableDeviceSecurityUseCase(instance()) } + bindProvider { ResetPasswordUseCase(instance()) } bindProvider { SaveZoomPreferenceUseCase(instance()) } + bindProvider { GetOrganDonationRegisterHostsUseCase(instance()) } + bindProvider { + val context = instance() + val resId = context.resources.getIdentifier( + "locale_config", + "xml", + context.packageName + ) + context.resources.getXml(resId) + } + bindProvider { DefaultXmlResourceParserWrapper(instance()) } + bindProvider { GetSupportedLanguagesFromXmlUseCase(instance()) } +} + +val settingsRepositoryModule = DI.Module("settingsRepositoryModule") { + bindProvider { CardWallRepository(prefs = instance(ApplicationPreferencesTag)) } + bindProvider { DefaultSettingsRepository(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/datasource/SettingsDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/datasource/SettingsDataSource.kt new file mode 100644 index 00000000..a22ecf3d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/datasource/SettingsDataSource.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.datasource + +import de.gematik.ti.erp.app.settings.model.SettingsData +import kotlinx.coroutines.flow.MutableStateFlow + +interface SettingsDataSource { + + val appVersion: SettingsData.AppVersion + val authentication: MutableStateFlow + val pharmacySearch: MutableStateFlow + val generalData: MutableStateFlow +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/LicenseModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/LicenseModels.kt index 735add83..15b4a7a1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/LicenseModels.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/LicenseModels.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.model diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/SettingsScreenActions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/SettingsScreenActions.kt new file mode 100644 index 00000000..881a82a9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/model/SettingsScreenActions.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.model + +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier + +data class SettingsActions( + val healthCardClickActions: HealthCardClickActions, + val legalClickActions: LegalClickActions, + val debugClickActions: DebugClickActions, + val onEnableZoom: () -> Unit, + val onDisableZoom: () -> Unit, + val onAllowScreenshots: (Boolean) -> Unit, + val onClickProductImprovementSettings: () -> Unit, + val onClickDeviceSecuritySettings: () -> Unit, + val onClickLanguageSettings: () -> Unit, + val onClickDemoModeEnd: () -> Unit, + val onClickDemoMode: () -> Unit, + val onClickEditProfile: (ProfileIdentifier) -> Unit, + val onClickMedicationPlan: () -> Unit, + val onClickOrganDonationRegister: () -> Unit +) + +data class HealthCardClickActions( + val onClickUnlockEgk: (UnlockMethod) -> Unit, + val onClickOrderHealthCard: () -> Unit +) + +data class LegalClickActions( + val onClickLegalNotice: () -> Unit, + val onClickDataProtection: () -> Unit, + val onClickOpenSourceLicences: () -> Unit, + val onClickAdditionalLicences: () -> Unit, + val onClickTermsOfUse: () -> Unit +) + +data class DebugClickActions( + val onClickDebug: () -> Unit, + val onClickBottomSheetShowcase: () -> Unit, + val onClickDemoTracking: () -> Unit +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsGraph.kt index 7b58849b..a3ddaa6b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsGraph.kt @@ -1,39 +1,42 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.navigation -import SettingsAccessibilityScreen import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation -import de.gematik.ti.erp.app.settings.ui.SettingsLegalNoticeScreen -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.settings.ui.SettingsOpenSourceLicencesScreen import de.gematik.ti.erp.app.navigation.renderComposable -import de.gematik.ti.erp.app.settings.ui.SettingsDataProtectionScreen -import de.gematik.ti.erp.app.settings.ui.SettingsAllowAnalyticsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsDeviceSecurityScreen -import de.gematik.ti.erp.app.settings.ui.SettingsAdditionalLicencesScreen -import de.gematik.ti.erp.app.settings.ui.SettingsProductImprovementsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsSetAppPasswordScreen -import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsTermsOfUseScreen +import de.gematik.ti.erp.app.navigation.slideInDown +import de.gematik.ti.erp.app.navigation.slideInRight +import de.gematik.ti.erp.app.navigation.slideOutLeft +import de.gematik.ti.erp.app.navigation.slideOutUp +import de.gematik.ti.erp.app.settings.ui.screens.SettingsAdditionalLicencesScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsAllowAnalyticsScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsDataProtectionScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsAppSecurityScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsLanguageScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsLegalNoticeScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsOpenSourceLicencesScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsProductImprovementsScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsSetAppPasswordScreen +import de.gematik.ti.erp.app.settings.ui.screens.SettingsTermsOfUseScreen @Suppress("LongMethod") fun NavGraphBuilder.settingsGraph( @@ -45,6 +48,9 @@ fun NavGraphBuilder.settingsGraph( route = SettingsNavigationScreens.subGraphName() ) { renderComposable( + stackEnterAnimation = { slideInDown() }, + stackExitAnimation = { slideOutUp() }, + popExitAnimation = { slideOutUp() }, route = SettingsNavigationScreens.SettingsScreen.route, arguments = SettingsNavigationScreens.SettingsScreen.arguments ) { navEntry -> @@ -53,15 +59,6 @@ fun NavGraphBuilder.settingsGraph( navBackStackEntry = navEntry ) } - renderComposable( - route = SettingsNavigationScreens.SettingsAccessibilityScreen.route, - arguments = SettingsNavigationScreens.SettingsAccessibilityScreen.arguments - ) { navEntry -> - SettingsAccessibilityScreen( - navController = navController, - navBackStackEntry = navEntry - ) - } renderComposable( route = SettingsNavigationScreens.SettingsProductImprovementsScreen.route, arguments = SettingsNavigationScreens.SettingsProductImprovementsScreen.arguments @@ -72,10 +69,12 @@ fun NavGraphBuilder.settingsGraph( ) } renderComposable( - route = SettingsNavigationScreens.SettingsDeviceSecurityScreen.route, - arguments = SettingsNavigationScreens.SettingsDeviceSecurityScreen.arguments + route = SettingsNavigationScreens.SettingsAppSecurityScreen.route, + arguments = SettingsNavigationScreens.SettingsAppSecurityScreen.arguments, + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() } ) { navEntry -> - SettingsDeviceSecurityScreen( + SettingsAppSecurityScreen( navController = navController, navBackStackEntry = navEntry ) @@ -84,12 +83,6 @@ fun NavGraphBuilder.settingsGraph( route = SettingsNavigationScreens.SettingsTermsOfUseScreen.route, arguments = SettingsNavigationScreens.SettingsTermsOfUseScreen.arguments ) { navEntry -> - @Requirement( - "O.Arch_8#3", - "O.Plat_11#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Webview containing local html without javascript" - ) SettingsTermsOfUseScreen( navController = navController, navBackStackEntry = navEntry @@ -108,12 +101,6 @@ fun NavGraphBuilder.settingsGraph( route = SettingsNavigationScreens.SettingsDataProtectionScreen.route, arguments = SettingsNavigationScreens.SettingsDataProtectionScreen.arguments ) { navEntry -> - @Requirement( - "O.Arch_8#4", - "O.Plat_11#4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Webview containing local html without javascript" - ) SettingsDataProtectionScreen( navController = navController, navBackStackEntry = navEntry @@ -148,12 +135,23 @@ fun NavGraphBuilder.settingsGraph( } renderComposable( route = SettingsNavigationScreens.SettingsSetAppPasswordScreen.route, - arguments = SettingsNavigationScreens.SettingsSetAppPasswordScreen.arguments + arguments = SettingsNavigationScreens.SettingsSetAppPasswordScreen.arguments, + stackEnterAnimation = { slideInRight() }, + stackExitAnimation = { slideOutLeft() } ) { navEntry -> SettingsSetAppPasswordScreen( navController = navController, navBackStackEntry = navEntry ) } + renderComposable( + route = SettingsNavigationScreens.SettingsLanguageScreen.route, + arguments = SettingsNavigationScreens.SettingsLanguageScreen.arguments + ) { navEntry -> + SettingsLanguageScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsRoutes.kt index 23ec3814..e636fdf6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/navigation/SettingsRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.navigation @@ -25,14 +25,14 @@ import de.gematik.ti.erp.app.navigation.Routes object SettingsNavigationScreens : NavigationRoutes { override fun subGraphName() = "settings" object SettingsScreen : Routes(NavigationRouteNames.SettingsScreen.name) - object SettingsAccessibilityScreen : Routes(NavigationRouteNames.SettingsAccessibilityScreen.name) object SettingsProductImprovementsScreen : Routes(NavigationRouteNames.SettingsProductImprovementScreen.name) object SettingsAllowAnalyticsScreen : Routes(NavigationRouteNames.SettingsAllowAnalyticsScreen.name) - object SettingsDeviceSecurityScreen : Routes(NavigationRouteNames.SettingsDeviceSecurityScreen.name) + object SettingsAppSecurityScreen : Routes(NavigationRouteNames.SettingsAppSecurityScreen.name) object SettingsSetAppPasswordScreen : Routes(NavigationRouteNames.SettingsSetAppPasswordScreen.name) object SettingsDataProtectionScreen : Routes(NavigationRouteNames.SettingsDataProtectionScreen.name) object SettingsTermsOfUseScreen : Routes(NavigationRouteNames.SettingsTermsOfUseScreen.name) object SettingsLegalNoticeScreen : Routes(NavigationRouteNames.SettingsLegalNoticeScreen.name) object SettingsOpenSourceLicencesScreen : Routes(NavigationRouteNames.SettingsOpenSourceLicencesScreen.name) object SettingsAdditionalLicencesScreen : Routes(NavigationRouteNames.SettingsAdditionalLicencesScreen.name) + object SettingsLanguageScreen : Routes(NavigationRouteNames.SettingsLanguageScreen.name) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AccessibilitySettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AccessibilitySettingsController.kt deleted file mode 100644 index c4ecad81..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AccessibilitySettingsController.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase -import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase -import de.gematik.ti.erp.app.settings.usecase.GetZoomStateUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveZoomPreferenceUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -class AccessibilitySettingsController( - getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, - private val allowScreenshotsUseCase: AllowScreenshotsUseCase, - private val saveZoomPreferenceUseCase: SaveZoomPreferenceUseCase, - getZoomStateUseCase: GetZoomStateUseCase, - private val scope: CoroutineScope -) { - private val zoomFlow = getZoomStateUseCase.invoke().map { SettingStatesData.ZoomState(it) } - - val zoomState - @Composable - get() = zoomFlow.collectAsStateWithLifecycle(SettingStatesData.defaultZoomState) - - private val screenShotsAllowed = - getScreenShotsAllowedUseCase.invoke() - - val screenShotsState - @Composable - get() = screenShotsAllowed.collectAsStateWithLifecycle(false) - - fun onAllowScreenshots(allow: Boolean) = scope.launch { - allowScreenshotsUseCase.invoke(allow) - } - - fun onEnableZoom() { - scope.launch { - saveZoomPreferenceUseCase.invoke(true) - } - } - - fun onDisableZoom() { - scope.launch { - saveZoomPreferenceUseCase.invoke(false) - } - } -} - -@Composable -fun rememberAccessibilitySettingsController(): AccessibilitySettingsController { - val getScreenShotsAllowedUseCase by rememberInstance() - val allowScreenshotsUseCase by rememberInstance() - val getZoomStateUseCase by rememberInstance() - val saveZoomPreferenceUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - return remember { - AccessibilitySettingsController( - getScreenShotsAllowedUseCase = getScreenShotsAllowedUseCase, - allowScreenshotsUseCase = allowScreenshotsUseCase, - saveZoomPreferenceUseCase = saveZoomPreferenceUseCase, - getZoomStateUseCase = getZoomStateUseCase, - scope = scope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AnalyticsSettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AnalyticsSettingsController.kt index 7176985e..1ce383e5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AnalyticsSettingsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AnalyticsSettingsController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.presentation @@ -21,51 +21,48 @@ package de.gematik.ti.erp.app.settings.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase -import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase +import de.gematik.ti.erp.app.analytics.usecase.StartTrackerUseCase +import de.gematik.ti.erp.app.analytics.usecase.StopTrackerUseCase import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance class AnalyticsSettingsController( - private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + private val startTrackerUseCase: StartTrackerUseCase, + private val stopTrackerUseCase: StopTrackerUseCase, private val scope: CoroutineScope ) { - - private val analyticsFlow by lazy { - isAnalyticsAllowedUseCase().map { SettingStatesData.AnalyticsState(it) } - } - - val analyticsState - @Composable - get() = analyticsFlow.collectAsStateWithLifecycle(SettingStatesData.defaultAnalyticsState) - @Requirement( - "O.Purp_5#3", + "O.Purp_5#4", sourceSpecification = "BSI-eRp-ePA", - rationale = "Enable usage analytics." + rationale = "Set allow/disallow analytics state." ) - fun changeAnalyticsAllowedState(boolean: Boolean) { + fun changeAnalyticsAllowedState(state: Boolean) { scope.launch { - changeAnalyticsStateUseCase.invoke(boolean) + changeAnalyticsStateUseCase.invoke(state) + when { + state -> startTrackerUseCase() + else -> stopTrackerUseCase() + } } } } @Composable fun rememberAnalyticsSettingsController(): AnalyticsSettingsController { - val isAnalyticsAllowedUseCase by rememberInstance() val changeAnalyticsStateUseCase by rememberInstance() + val startTrackerUseCase by rememberInstance() + val stopTrackerUseCase by rememberInstance() val scope = rememberCoroutineScope() return remember { AnalyticsSettingsController( - isAnalyticsAllowedUseCase = isAnalyticsAllowedUseCase, changeAnalyticsStateUseCase = changeAnalyticsStateUseCase, + startTrackerUseCase = startTrackerUseCase, + stopTrackerUseCase = stopTrackerUseCase, scope = scope ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AppSecuritySettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AppSecuritySettingsController.kt new file mode 100644 index 00000000..34b2fddd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/AppSecuritySettingsController.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.presentation + +import android.app.KeyguardManager +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.usecase.DisableDeviceSecurityUseCase +import de.gematik.ti.erp.app.settings.usecase.GetAuthenticationUseCase +import de.gematik.ti.erp.app.settings.usecase.EnableDeviceSecurityUseCase +import de.gematik.ti.erp.app.settings.usecase.ResetPasswordUseCase +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class AppSecuritySettingsController( + private val enableDeviceSecurityUseCase: EnableDeviceSecurityUseCase, + private val disableDeviceSecurityUseCase: DisableDeviceSecurityUseCase, + private val resetPasswordUseCase: ResetPasswordUseCase, + private val getAuthenticationUseCase: GetAuthenticationUseCase, + private val biometricPromptBuilder: BiometricPromptBuilder, + private val promptInfo: BiometricPrompt.PromptInfo, + private val keyguardManager: KeyguardManager +) : Controller() { + val events = AppSecuritySettingsEvents() + + val authenticationState by lazy { + getAuthenticationUseCase().stateIn( + controllerScope, + started = SharingStarted.WhileSubscribed(), + initialValue = defaultAuthentication + ) + } + + fun onSwitchDeviceSecurityAuthentication(isChecked: Boolean) { + controllerScope.launch { + when { + !keyguardManager.isDeviceSecure -> { + events.enrollBiometryEvent.trigger(Unit) + } + isChecked -> { + onAuthenticateWithDeviceSecurity() + } + !isChecked -> { + disableDeviceSecurityUseCase() + } + } + } + } + + private fun onAuthenticateWithDeviceSecurity() { + val prompt = biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + onEnableDeviceSecurity() + }, + onError = { errorMessage, errorCode -> + // handle error + } + ) + prompt.authenticate(promptInfo) + } + + private fun onEnableDeviceSecurity() { + controllerScope.launch { + enableDeviceSecurityUseCase() + } + } + + fun onSwitchPasswordAuthentication( + isChecked: Boolean + ) { + controllerScope.launch { + when (isChecked) { + true -> { + events.openPasswordScreenEvent.trigger(Unit) + } + false -> { + resetPasswordUseCase() + } + } + } + } +} + +val defaultAuthentication = SettingsData.Authentication( + password = null, + deviceSecurity = false, + failedAuthenticationAttempts = 0 +) + +data class AppSecuritySettingsEvents( + val enrollBiometryEvent: ComposableEvent = ComposableEvent(), + val openPasswordScreenEvent: ComposableEvent = ComposableEvent() +) + +@Composable +fun rememberAppSecuritySettingsController(): AppSecuritySettingsController { + val enableDeviceSecurityUseCase by rememberInstance() + val disableDeviceSecurityUseCase by rememberInstance() + val resetPasswordUseCase by rememberInstance() + val getAuthenticationUseCase by rememberInstance() + + val activity = LocalActivity.current + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } + val promptInfo = biometricPromptBuilder.buildPromptInfoWithAllAuthenticatorsAvailable( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info) + ) + + val context = LocalContext.current + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + + return remember { + AppSecuritySettingsController( + enableDeviceSecurityUseCase = enableDeviceSecurityUseCase, + disableDeviceSecurityUseCase = disableDeviceSecurityUseCase, + resetPasswordUseCase = resetPasswordUseCase, + getAuthenticationUseCase = getAuthenticationUseCase, + biometricPromptBuilder = biometricPromptBuilder, + promptInfo = promptInfo, + keyguardManager = keyguardManager + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/DeviceSecuritySettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/DeviceSecuritySettingsController.kt deleted file mode 100644 index d618e4f9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/DeviceSecuritySettingsController.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.presentation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.settings.usecase.GetAuthenticationModeUseCase -import de.gematik.ti.erp.app.settings.usecase.SaveDeviceSecurityUseCase -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -class DeviceSecuritySettingsController( - private val saveDeviceSecurityUseCase: SaveDeviceSecurityUseCase, - getAuthenticationModeUseCase: GetAuthenticationModeUseCase, - private val scope: CoroutineScope -) { - private val authenticationModeFlow = getAuthenticationModeUseCase.invoke().map { - SettingStatesData.AuthenticationModeState( - it - ) - } - - val authenticationModeState - @Composable - get() = authenticationModeFlow.collectAsStateWithLifecycle(SettingStatesData.defaultAuthenticationState) - - fun onSelectDeviceSecurityAuthenticationMode() { - scope.launch { - saveDeviceSecurityUseCase.invoke() - } - } -} - -@Composable -fun rememberDeviceSecuritySettingsController(): DeviceSecuritySettingsController { - val saveDeviceSecurityUseCase by rememberInstance() - val getAuthenticationModeUseCase by rememberInstance() - val scope = rememberCoroutineScope() - - return remember { - DeviceSecuritySettingsController( - saveDeviceSecurityUseCase = saveDeviceSecurityUseCase, - getAuthenticationModeUseCase = getAuthenticationModeUseCase, - scope = scope - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/PasswordSettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/PasswordSettingsController.kt index 456180af..a5cb8873 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/PasswordSettingsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/PasswordSettingsController.kt @@ -1,49 +1,63 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import de.gematik.ti.erp.app.settings.usecase.SavePasswordUseCase -import kotlinx.coroutines.CoroutineScope +import androidx.compose.ui.platform.LocalContext +import com.nulabinc.zxcvbn.StandardDictionaries +import com.nulabinc.zxcvbn.Zxcvbn +import com.nulabinc.zxcvbn.ZxcvbnBuilder +import com.nulabinc.zxcvbn.io.Resource +import com.nulabinc.zxcvbn.matchers.DictionaryLoader +import de.gematik.ti.erp.app.settings.usecase.SetPasswordUseCase +import de.gematik.ti.erp.app.utils.compose.presentation.PasswordFieldsController import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance class PasswordSettingsController( - private val savePasswordUseCase: SavePasswordUseCase, - private val scope: CoroutineScope -) { - fun selectPasswordAsAuthenticationMode(password: String) = scope.launch { - savePasswordUseCase.invoke(password) + private val setPasswordUseCase: SetPasswordUseCase, + passwordStrengthEvaluator: Zxcvbn +) : PasswordFieldsController(passwordStrengthEvaluator) { + fun setAppPassword() = controllerScope.launch { + setPasswordUseCase.invoke(passwordFieldsState.value.password) } } @Composable fun rememberPasswordSettingsController(): PasswordSettingsController { - val savePasswordUseCase by rememberInstance() - val scope = rememberCoroutineScope() + val setPasswordUseCase by rememberInstance() + + val context = LocalContext.current + val assetManager = context.assets + val germanDictionaryFile = assetManager.open("german_dictionary.txt") + val germanDictionaryResource = Resource { germanDictionaryFile } + val passwordStrengthEvaluator = ZxcvbnBuilder().dictionaries( + StandardDictionaries.loadAllDictionaries() + ) + .dictionary(DictionaryLoader("german_dictionary", germanDictionaryResource).load()) + .build() return remember { PasswordSettingsController( - savePasswordUseCase = savePasswordUseCase, - scope = scope + setPasswordUseCase = setPasswordUseCase, + passwordStrengthEvaluator = passwordStrengthEvaluator ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/ProductsImprovementsSettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/ProductsImprovementsSettingsController.kt index 13cc5daa..cc0727eb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/ProductsImprovementsSettingsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/ProductsImprovementsSettingsController.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsController.kt new file mode 100644 index 00000000..349ea648 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsController.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.viewModelScope +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.di.EndpointHelper +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetZoomStateUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveZoomPreferenceUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +class SettingsController( + getProfilesUseCase: GetProfilesUseCase, + getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, + getZoomStateUseCase: GetZoomStateUseCase, + featureToggleManager: FeatureToggleManager, + private val allowScreenshotsUseCase: AllowScreenshotsUseCase, + private val saveZoomPreferenceUseCase: SaveZoomPreferenceUseCase, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val endpointHelper: EndpointHelper +) : Controller() { + + private val zoomFlow = getZoomStateUseCase.invoke().map { SettingStatesData.ZoomState(it) } + private val screenShotsAllowedFlow = getScreenShotsAllowedUseCase.invoke() + + val profiles: StateFlow> = getProfilesUseCase() + .stateIn(controllerScope, SharingStarted.WhileSubscribed(), emptyList()) + + val zoomState: StateFlow = zoomFlow.stateIn( + controllerScope, + SharingStarted.WhileSubscribed(), + SettingStatesData.defaultZoomState + ) + + val screenShotsState: StateFlow = screenShotsAllowedFlow.stateIn( + controllerScope, + SharingStarted.WhileSubscribed(), + false + ) + + val allowScreenshotsEvent = ComposableEvent() + + val isMedicationPlanEnabled: StateFlow = + featureToggleManager.isFeatureEnabled(Features.MEDICATION_PLAN) + .stateIn( + controllerScope, + SharingStarted.WhileSubscribed(), + false + ) + + fun onAllowScreenshots(allow: Boolean) = viewModelScope.launch { + if (allow) { + allowScreenshotsEvent.trigger(Unit) + } else { + allowScreenshotsUseCase.invoke(false) + } + } + + val intentEvent = ComposableEvent() + + fun confirmAllowScreenshots() = viewModelScope.launch { + allowScreenshotsUseCase.invoke(true) + } + + fun onEnableZoom() { + viewModelScope.launch { + saveZoomPreferenceUseCase.invoke(true) + } + } + + fun onDisableZoom() { + viewModelScope.launch { + saveZoomPreferenceUseCase.invoke(false) + } + } + + fun createOrganDonationRegisterIntent() { + controllerScope.launch { + val profile = getActiveProfileUseCase.invoke().first() + var url = endpointHelper.getOrganDonationRegisterInfoHost() + + profile.let { + val token = profile.ssoTokenScope + + if (token is IdpData.ExternalAuthenticationToken) { + val iss = token.authenticatorId + val issSafe = URLEncoder.encode(iss, StandardCharsets.UTF_8.toString()) + url = "${endpointHelper.getOrganDonationRegisterIntentHost()}?iss=$issSafe" + } + } + intentEvent.trigger(url) + } + } +} + +@Composable +fun rememberSettingsController(): SettingsController { + val getProfilesUseCase by rememberInstance() + val getScreenShotsAllowedUseCase by rememberInstance() + val allowScreenshotsUseCase by rememberInstance() + val getZoomStateUseCase by rememberInstance() + val saveZoomPreferenceUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val endpointHelper by rememberInstance() + val featureToggleManager by rememberInstance() + + return remember { + SettingsController( + getProfilesUseCase = getProfilesUseCase, + getScreenShotsAllowedUseCase = getScreenShotsAllowedUseCase, + allowScreenshotsUseCase = allowScreenshotsUseCase, + saveZoomPreferenceUseCase = saveZoomPreferenceUseCase, + getZoomStateUseCase = getZoomStateUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + endpointHelper = endpointHelper, + featureToggleManager = featureToggleManager + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsLanguageScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsLanguageScreenController.kt new file mode 100644 index 00000000..f46ac48c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsLanguageScreenController.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.usecase.GetSupportedLanguagesFromXmlUseCase +import org.kodein.di.compose.rememberInstance + +class SettingsLanguageScreenController( + val getSupportedLanguagesFromXmlUseCase: GetSupportedLanguagesFromXmlUseCase +) : Controller() { + + val languageList by lazy { + getSupportedLanguagesFromXmlUseCase() + } +} + +enum class LanguageCode(val code: String, val resource: Int) { + DE("de", R.string.language_selection_de), // default language + AR("ar", R.string.language_selection_ar), + BG("bg", R.string.language_selection_bg), + CS("cs", R.string.language_selection_cs), + DA("da", R.string.language_selection_da), + EN("en", R.string.language_selection_en), + FR("fr", R.string.language_selection_fr), + IW("iw", R.string.language_selection_he), + IT("it", R.string.language_selection_it), + NL("nl", R.string.language_selection_nl), + PL("pl", R.string.language_selection_pl), + RO("ro", R.string.language_selection_ro), + RU("ru", R.string.language_selection_ru), + TR("tr", R.string.language_selection_tr), + UK("uk", R.string.language_selection_uk); + + @Composable + fun mapToName() = stringResource(resource) + + companion object { + val codes = entries.map { it.code } + + fun fromCode(code: String) = entries.firstOrNull { it.code == code } + } +} + +/** + * Maps a language code to its name + * + * TODO: team session - this should be moved to a more appropriate place (utility class?) + * + * @param code The language code to map + * @return The name of the language + */ +@Composable +fun mapLanguageCodeToName(code: String): String? = LanguageCode.fromCode(code)?.mapToName() + +@Composable +fun rememberSettingsLanguageScreenController(): SettingsLanguageScreenController { + val getSupportedLanguagesFromXmlUseCase by rememberInstance() + return remember { + SettingsLanguageScreenController(getSupportedLanguagesFromXmlUseCase) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsStatesData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsStatesData.kt index 0c6c1822..81bb7288 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsStatesData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/presentation/SettingsStatesData.kt @@ -1,25 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.presentation import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.settings.model.SettingsData object SettingStatesData { @@ -30,13 +29,6 @@ object SettingStatesData { val defaultAnalyticsState = AnalyticsState(analyticsAllowed = false) - @Immutable - data class AuthenticationModeState( - val authenticationMode: SettingsData.AuthenticationMode - ) - - val defaultAuthenticationState = AuthenticationModeState(SettingsData.AuthenticationMode.Unspecified) - @Immutable data class ZoomState( val zoomEnabled: Boolean diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt index 76b2c21a..c52918ef 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.repository diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAccessibilityScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAccessibilityScreen.kt deleted file mode 100644 index ff19e34a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAccessibilityScreen.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Camera -import androidx.compose.material.icons.rounded.ZoomIn -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.settings.presentation.SettingStatesData -import de.gematik.ti.erp.app.settings.presentation.rememberAccessibilitySettingsController -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.LabeledSwitch -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class SettingsAccessibilityScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val settingsController = rememberAccessibilitySettingsController() - val zoomState by settingsController.zoomState - val listState = rememberLazyListState() - val screenShotState by settingsController.screenShotsState - - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.settings_accessibility_headline), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = navController::popBackStack - ) { contentPadding -> - SettingsAccessibilityScreenContent( - contentPadding, - listState, - onEnableZoom = { settingsController.onEnableZoom() }, - onDisableZoom = { settingsController.onDisableZoom() }, - onAllowScreenshots = { settingsController.onAllowScreenshots(allow = it) }, - screenShotState, - zoomState - ) - } - } -} - -@Composable -private fun SettingsAccessibilityScreenContent( - contentPadding: PaddingValues, - listState: LazyListState, - onEnableZoom: () -> Unit, - onDisableZoom: () -> Unit, - onAllowScreenshots: (Boolean) -> Unit, - screenShotState: Boolean, - zoomState: SettingStatesData.ZoomState -) { - LazyColumn( - contentPadding = contentPadding, - state = listState - ) { - item { - SpacerMedium() - ZoomSection(zoomChecked = zoomState.zoomEnabled) { zoomEnabled -> - when (zoomEnabled) { - true -> onEnableZoom() - false -> onDisableZoom() - } - } - } - item { - AllowScreenShotsSection( - screenShotsAllowed = screenShotState - ) { allow -> - onAllowScreenshots(allow) - } - } - } -} - -@Composable -private fun ZoomSection( - modifier: Modifier = Modifier, - zoomChecked: Boolean, - onZoomChange: (Boolean) -> Unit -) { - LabeledSwitch( - modifier = modifier, - checked = zoomChecked, - onCheckedChange = onZoomChange, - icon = Icons.Rounded.ZoomIn, - header = stringResource(R.string.settings_accessibility_zoom_toggle), - description = stringResource(R.string.settings_accessibility_zoom_info) - ) -} - -@Composable -private fun AllowScreenShotsSection( - modifier: Modifier = Modifier, - screenShotsAllowed: Boolean, - onAllowScreenshots: (Boolean) -> Unit -) { - LabeledSwitch( - modifier = modifier, - checked = !screenShotsAllowed, - onCheckedChange = { checked -> - onAllowScreenshots(!checked) - }, - icon = Icons.Rounded.Camera, - header = stringResource(R.string.settings_screenshots_toggle_text), - description = stringResource(R.string.settings_screenshots_description) - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAdditionalLicencesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAdditionalLicencesScreen.kt deleted file mode 100644 index 64051f2e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAdditionalLicencesScreen.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource - -class SettingsAdditionalLicencesScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.settings_licence_pharmacy_search), - navigationMode = NavigationBarMode.Close, - onBack = navController::popBackStack, - listState = listState - ) { - SettingsAdditionalLicencesScreenContent( - listState - ) - } - } -} - -@Composable -fun SettingsAdditionalLicencesScreenContent( - listState: LazyListState -) { - LazyColumn( - modifier = Modifier - .padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - top = PaddingDefaults.Medium, - bottom = (PaddingDefaults.XLarge * 2) - ), - state = listState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) - ) { - item { - Text( - stringResource(R.string.license_pharmacy_search_description), - style = AppTheme.typography.body1, - color = AppTheme.colors.neutral999 - ) - } - item { - val link = - provideLinkForString( - stringResource(id = R.string.license_pharmacy_search_web_link), - annotation = stringResource(id = R.string.license_pharmacy_search_web_link), - tag = "URL", - linkColor = AppTheme.colors.primary500 - ) - - val uriHandler = LocalUriHandler.current - - ClickableTaggedText( - annotatedStringResource(R.string.license_pharmacy_search_web_link_info, link), - style = AppTheme.typography.body1, - onClick = { range -> - uriHandler.openUri(range.item) - } - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreen.kt deleted file mode 100644 index 87a34e54..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreen.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar -import de.gematik.ti.erp.app.settings.presentation.rememberAnalyticsSettingsController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.annotatedStringBold -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import de.gematik.ti.erp.app.utils.compose.shortToast - -@Requirement( - "A_19087", - "A_19088#1", - "A_19091#1", - "A_19092", - "A_19181-01#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Display opt-in for analytics. Full app functionality is available also without opting in." -) -class SettingsAllowAnalyticsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val settingsController = rememberAnalyticsSettingsController() - val context = LocalContext.current - val allowStars = stringResource(R.string.settings_tracking_allow_emoji) - val allowText = annotatedStringResource( - R.string.settings_tracking_allow_info, - annotatedStringBold(allowStars) - ).toString() - val disAllowToast = stringResource(R.string.settings_tracking_disallow_info) - val lazyListState = rememberLazyListState() - - AnimatedElevationScaffold( - modifier = Modifier.navigationBarsPadding(), - navigationMode = NavigationBarMode.Back, - topBarTitle = stringResource(R.string.settings_tracking_allow_title), - onBack = { - settingsController.changeAnalyticsAllowedState(false) - context.shortToast(disAllowToast) - navController.popBackStack() - }, - listState = lazyListState, - bottomBar = { - @Requirement( - "A_19091#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "User confirms the opt in" - ) - OnboardingBottomBar( - buttonText = stringResource(R.string.settings_tracking_allow_button), - onButtonClick = { - settingsController.changeAnalyticsAllowedState(true) - context.shortToast(allowText) - navController.popBackStack() - }, - buttonEnabled = true, - info = null, - buttonModifier = Modifier.testTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) - ) - } - ) { contentPadding -> - SettingsAllowAnalyticsScreenContent( - lazyListState, - contentPadding - ) - } - } -} - -@Composable -private fun SettingsAllowAnalyticsScreenContent( - lazyListState: LazyListState, - contentPadding: PaddingValues -) { - LazyColumn( - state = lazyListState, - modifier = Modifier - .wrapContentSize() - .padding( - horizontal = PaddingDefaults.Medium - ) - .padding(bottom = contentPadding.calculateBottomPadding()) - .testTag(TestTag.Onboarding.Analytics.ScreenContent) - ) { - item { - @Requirement( - "A_19089#1", - "A_19090", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Display explanation of data processing for analytics opt-in" - ) - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .semantics(mergeDescendants = true) {} - ) { - Text( - stringResource(R.string.settings_tracking_dialog_title), - style = AppTheme.typography.h6, - modifier = Modifier.padding( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Large - ) - ) - Text( - stringResource(R.string.settings_tracking_dialog_text_1), - style = AppTheme.typography.body1, - modifier = Modifier.padding(bottom = PaddingDefaults.Small) - ) - Text( - stringResource(R.string.settings_tracking_dialog_text_2), - style = AppTheme.typography.body1, - modifier = Modifier.padding(bottom = PaddingDefaults.Small) - ) - Text( - stringResource(R.string.settings_tracking_dialog_text_3), - style = AppTheme.typography.body1, - modifier = Modifier.padding(bottom = PaddingDefaults.Medium) - ) - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDataProtectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDataProtectionScreen.kt deleted file mode 100644 index f472f6d6..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDataProtectionScreen.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.utils.extensions.getUriDataTerms -import de.gematik.ti.erp.app.webview.WebViewScreen - -@Requirement( - "O.Purp_1#2", - "O.Arch_8#6", - "O.Plat_11#6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Display data privacy as part of the onboarding. " + - "Webview containing local html without javascript." -) -class SettingsDataProtectionScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - WebViewScreen( - title = stringResource(R.string.onb_data_consent), - onBack = navController::popBackStack, - url = getUriDataTerms() - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDeviceSecurityScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDeviceSecurityScreen.kt deleted file mode 100644 index a358173b..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsDeviceSecurityScreen.kt +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Fingerprint -import androidx.compose.material.icons.outlined.Security -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material.icons.rounded.RadioButtonUnchecked -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens -import de.gematik.ti.erp.app.settings.presentation.SettingStatesData -import de.gematik.ti.erp.app.settings.presentation.rememberDeviceSecuritySettingsController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.userauthentication.ui.BiometricPromptWrapper -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class SettingsDeviceSecurityScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val settingsController = rememberDeviceSecuritySettingsController() - val authenticationModeState by settingsController.authenticationModeState - val listState = rememberLazyListState() - var showBiometricPrompt by rememberSaveable { mutableStateOf(false) } - - if (showBiometricPrompt) { - BiometricPromptWrapper( - title = stringResource(R.string.auth_prompt_headline), - description = "", - negativeButton = stringResource(R.string.auth_prompt_cancel), - onAuthenticated = { - settingsController.onSelectDeviceSecurityAuthenticationMode() - showBiometricPrompt = false - }, - onCancel = { - showBiometricPrompt = false - }, - onAuthenticationError = { - showBiometricPrompt = false - }, - onAuthenticationSoftError = { - } - ) - } - - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.settings_device_security_header), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = navController::popBackStack - ) { contentPadding -> - SettingsDeviceSecurityScreenContent( - contentPadding, - listState, - authenticationModeState, - onNavigateToPasswordScreen = { - navController.navigate(SettingsNavigationScreens.SettingsSetAppPasswordScreen.path()) - }, - onShowBiometricPrompt = { showBiometricPrompt = it } - ) - } - } -} - -@Composable -private fun SettingsDeviceSecurityScreenContent( - contentPadding: PaddingValues, - listState: LazyListState, - authenticationModeState: SettingStatesData.AuthenticationModeState, - onNavigateToPasswordScreen: () -> Unit, - onShowBiometricPrompt: (Boolean) -> Unit -) { - LazyColumn( - contentPadding = contentPadding, - state = listState - ) { - item { - SpacerMedium() - AuthenticationModeCard( - Icons.Outlined.Fingerprint, - checked = authenticationModeState.authenticationMode is - SettingsData.AuthenticationMode.DeviceSecurity, - headline = stringResource(R.string.settings_appprotection_device_security_header), - info = stringResource(R.string.settings_appprotection_device_security_info), - deviceSecurity = true - ) { - onShowBiometricPrompt(true) - } - } - item { - @Requirement( - "O.Pass_3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The user may change the app passwords within the settings." - ) - AuthenticationModeCard( - Icons.Outlined.Security, - checked = authenticationModeState.authenticationMode is - SettingsData.AuthenticationMode.Password, - headline = stringResource(R.string.settings_appprotection_mode_password_headline), - info = stringResource(R.string.settings_appprotection_mode_password_info) - ) { - onNavigateToPasswordScreen() - } - } - } -} - -@Composable -private fun AuthenticationModeCard( - icon: ImageVector, - checked: Boolean, - headline: String, - info: String, - deviceSecurity: Boolean = false, - enabled: Boolean = true, - onClick: () -> Unit -) { - var showAllowDeviceSecurity by remember { mutableStateOf(false) } - - if (deviceSecurity && showAllowDeviceSecurity && !checked) { - CommonAlertDialog( - header = stringResource(R.string.settings_biometric_dialog_title), - info = stringResource(R.string.settings_biometric_dialog_text), - actionText = stringResource(R.string.settings_device_security_allow), - cancelText = stringResource(R.string.cancel), - onCancel = { showAllowDeviceSecurity = false }, - onClickAction = { - onClick() - showAllowDeviceSecurity = false - } - ) - } - - val alpha = remember { Animatable(0.0f) } - - LaunchedEffect(checked) { - if (checked) { - alpha.animateTo(1.0f) - } else { - alpha.animateTo(0.0f) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .clickable( - onClick = { - if (deviceSecurity) { - showAllowDeviceSecurity = true - } else { - onClick() - } - }, - enabled = enabled - ) - .padding(PaddingDefaults.Medium) - ) { - Icon( - icon, - null, - tint = AppTheme.colors.primary500, - modifier = Modifier.padding(end = PaddingDefaults.Small) - ) - Column(modifier = Modifier.weight(1.0f)) { - Text( - text = headline, - style = AppTheme.typography.body1 - ) - Text( - text = info, - style = AppTheme.typography.body2l - ) - } - - Box(modifier = Modifier.align(Alignment.CenterVertically)) { - Icon( - Icons.Rounded.RadioButtonUnchecked, - null, - tint = AppTheme.colors.neutral400 - ) - Icon( - Icons.Rounded.CheckCircle, - null, - tint = AppTheme.colors.primary600, - modifier = Modifier.alpha(alpha.value) - ) - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreen.kt deleted file mode 100644 index 665e50c3..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreen.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.model.License -import de.gematik.ti.erp.app.settings.model.LicenseEntry -import de.gematik.ti.erp.app.settings.model.parseLicenses -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PreviewAppTheme -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight - -const val LicenseFileUri = "open_source_licenses.json" - -@Requirement( - "O.Purp_7#1", - "O.Arch_7", - "O.Source_10", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Dependencies are only included if really necessary. " + - "We think about including only sub packages, but as most dependencies are very small, " + - "this is hardly used. We are using proguard for removing unused dependency functionality." + - "By default, Stack Smashing Protection is active." + - "A vulnerability analysis on 3rd party libraries is carried out using Owasp Dependency Check." -) -class SettingsOpenSourceLicencesScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val listState = rememberLazyListState() - AnimatedElevationScaffold( - navigationMode = NavigationBarMode.Back, - listState = listState, - topBarTitle = stringResource(R.string.settings_legal_licences), - onBack = navController::popBackStack - ) { - val licenses = rememberLicenses() - - val insetPaddings = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - LazyColumn( - modifier = Modifier.padding(), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - contentPadding = PaddingValues( - start = PaddingDefaults.Medium, - top = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium + insetPaddings.calculateBottomPadding() - ) - ) { - licenses.forEach { - item { - LicenseItem(item = it) - } - } - } - } - } -} - -@Composable -private fun rememberLicenses(): List { - val context = LocalContext.current - return remember { - val json = context.assets.open(LicenseFileUri).bufferedReader().readText() - parseLicenses(json) - } -} - -@Composable -private fun LicenseItem( - modifier: Modifier = Modifier, - item: LicenseEntry -) { - val title = buildAnnotatedString { - append(item.project) - if (!item.version.isNullOrBlank()) { - append(" (${item.version})") - } - if (!item.year.isNullOrBlank()) { - append(" ${item.year}") - } - } - - val uriHandler = LocalUriHandler.current - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - Text(title, style = AppTheme.typography.h6) - Text(item.dependency, style = AppTheme.typography.body2l, fontStyle = FontStyle.Italic) - item.description?.let { - Text(item.description, style = AppTheme.typography.body2l, fontStyle = FontStyle.Italic) - } - item.developers.takeIf { it.isNotEmpty() }?.let { - Text(item.developers.joinToString(), style = AppTheme.typography.body2) - } - item.url?.let { - ClickableTaggedText( - text = annotatedLinkStringLight(item.url, item.url), - style = AppTheme.typography.body2 - ) { - if (it.tag == "URL") { - uriHandler.openUri(it.item) - } - } - } - SpacerSmall() - item.licenses.forEach { - ClickableTaggedText( - text = annotatedLinkStringLight(it.licenseUrl, it.license), - style = AppTheme.typography.body2 - ) { - if (it.tag == "URL") { - uriHandler.openUri(it.item) - } - } - } - } -} - -@Preview -@Composable -private fun LicenseItemPreview() { - PreviewAppTheme { - LicenseItem( - item = LicenseEntry( - project = "Test 1234", - description = "Some short description", - version = "1.2.3", - developers = listOf( - "Some Author", - "Another Author", - "And Another One" - ), - url = "https://localhost/123456", - year = "2022", - licenses = listOf( - License( - "Apache License, Version 2.0", - "https://www.apache.org/licenses/LICENSE-2.0" - ), - License( - "Apache License, Version 2.0", - "https://www.apache.org/licenses/LICENSE-2.0" - ) - ), - dependency = "de.abc.def:1.2.3" - ) - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreen.kt deleted file mode 100644 index 7d6a7b11..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreen.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import android.content.Context -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.OpenInBrowser -import androidx.compose.material.icons.rounded.Timeline -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.semantics -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens -import de.gematik.ti.erp.app.settings.presentation.rememberProductsImprovementsSettingsController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.LabeledSwitch -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.handleIntent -import de.gematik.ti.erp.app.utils.compose.provideWebIntent -import de.gematik.ti.erp.app.utils.compose.shortToast - -class SettingsProductImprovementsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val settingsController = rememberProductsImprovementsSettingsController() - val context = LocalContext.current - val listState = rememberLazyListState() - val analyticsState by settingsController.analyticsState - var isAnalyticsAllowed by remember( - analyticsState.analyticsAllowed - ) { mutableStateOf(analyticsState.analyticsAllowed) } - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.settings_product_improvement_headline), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = navController::popBackStack - ) { contentPadding -> - SettingsProductImprovementsScreenContent( - contentPadding, - listState, - onDisallowAnalytics = { settingsController.changeAnalyticsAllowedState(false) }, - isAnalyticsAllowed = isAnalyticsAllowed, - onIsAnalyticsAllowedChange = { isAnalyticsAllowed = !isAnalyticsAllowed }, - context = context, - navigateToAnalytics = { - navController.navigate(SettingsNavigationScreens.SettingsAllowAnalyticsScreen.path()) - } - ) - } - } -} - -@Composable -private fun SettingsProductImprovementsScreenContent( - contentPadding: PaddingValues, - listState: LazyListState, - isAnalyticsAllowed: Boolean, - onIsAnalyticsAllowedChange: () -> Unit, - onDisallowAnalytics: () -> Unit, - context: Context, - navigateToAnalytics: () -> Unit -) { - val disallowInfo = stringResource(R.string.settings_tracking_disallow_info) - LazyColumn( - contentPadding = contentPadding, - state = listState - ) { - item { - SpacerMedium() - AnalyticsSection( - isAnalyticsAllowed - ) { state -> - onIsAnalyticsAllowedChange() - if (!state) { - onDisallowAnalytics() - context.shortToast(disallowInfo) - } else { - @Requirement( - "O.Purp_5#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The agreement to the use of the analytics framework could be revoked. " + - "But other agreements cannot be revoked, since the app could not operate properly." - ) - @Requirement( - "A_19982", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "The agreement to the use of the analytics framework could be revoked. " + - "But other agreements cannot be revoked, since the app could not operate properly." - ) - navigateToAnalytics() - } - } - } - item { - SurveySection() - } - } -} - -@Composable -private fun SurveySection() { - val context = LocalContext.current - val surveyAddress = stringResource(R.string.settings_contact_survey_address) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = { context.handleIntent(provideWebIntent(surveyAddress)) }, - role = Role.Button - ) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(Icons.Outlined.OpenInBrowser, null, tint = AppTheme.colors.primary600) - SpacerSmall() - Column( - modifier = Modifier - .weight(1.0f) - .padding(horizontal = PaddingDefaults.Small) - ) { - Text( - text = stringResource(R.string.settings_contact_feedback), - style = AppTheme.typography.body1 - ) - Text( - text = stringResource(R.string.settings_contact_feedback_description), - style = AppTheme.typography.body2l - ) - } - } -} - -@Requirement( - "A_19088#2", - "A_20187#3", - "A_19088", - "A_19089#2", - "A_19097", - "A_19181-01#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "User can opt-in and opt-out of analytics" -) -@Requirement( - "O.Purp_5#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Toggle within Settings to enable and disable usage analytics." -) -@Requirement( - "O.Purp_6#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Current Analytics state is inspectable by the user, as most of the user deciscions are client side " + - "no history is available. Only the current state can be inspected." -) -@Composable -private fun AnalyticsSection( - analyticsAllowed: Boolean, - modifier: Modifier = Modifier, - onCheckedChange: (Boolean) -> Unit -) { - LabeledSwitch( - checked = analyticsAllowed, - onCheckedChange = onCheckedChange, - modifier = modifier, - icon = Icons.Rounded.Timeline, - header = stringResource(R.string.settings_allow_analytics_header), - description = stringResource(R.string.settings_allow_analytics_info) - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt deleted file mode 100644 index 1c282514..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt +++ /dev/null @@ -1,870 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("MaximumLineLength") - -package de.gematik.ti.erp.app.settings.ui - -import android.content.Context -import androidx.activity.ComponentActivity -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AccessibilityNew -import androidx.compose.material.icons.outlined.ChecklistRtl -import androidx.compose.material.icons.outlined.Code -import androidx.compose.material.icons.outlined.HelpOutline -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.KeyboardArrowRight -import androidx.compose.material.icons.outlined.LockOpen -import androidx.compose.material.icons.outlined.Mail -import androidx.compose.material.icons.outlined.PrivacyTip -import androidx.compose.material.icons.outlined.Security -import androidx.compose.material.icons.outlined.SettingsInputComposite -import androidx.compose.material.icons.outlined.Source -import androidx.compose.material.icons.outlined.Timeline -import androidx.compose.material.icons.outlined.TireRepair -import androidx.compose.material.icons.outlined.Wysiwyg -import androidx.compose.material.icons.rounded.PersonOutline -import androidx.compose.material.icons.rounded.Phone -import androidx.compose.material.icons.rounded.PhoneAndroid -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.navigation.TrackingScreenRoutes -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.demomode.DemoModeIntent -import de.gematik.ti.erp.app.demomode.DemoModeObserver -import de.gematik.ti.erp.app.demomode.startAppWithDemoMode -import de.gematik.ti.erp.app.demomode.startAppWithNormalMode -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.info.BuildConfigInformation -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.pharmacy.navigation.PharmacyRoutes -import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes -import de.gematik.ti.erp.app.profiles.presentation.ProfileController -import de.gematik.ti.erp.app.profiles.presentation.rememberProfileController -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.Avatar -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName -import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.buildFeedbackBodyWithDeviceInfo -import de.gematik.ti.erp.app.utils.compose.AlertDialog -import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.handleIntent -import de.gematik.ti.erp.app.utils.compose.providePhoneIntent -import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension -import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar -import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName -import de.gematik.ti.erp.app.utils.openMailClient -import org.kodein.di.compose.rememberInstance -import java.util.Locale - -class SettingsScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val buildConfig by rememberInstance() - val context = LocalContext.current - val localActivity = LocalActivity.current - val demoModeObserver = localActivity as? DemoModeObserver - val isDemomode = demoModeObserver?.isDemoMode() ?: false - val profilesController = rememberProfileController() - val profilesState by profilesController.getProfilesState() - val listState = rememberLazyListState() - - Scaffold( - modifier = Modifier - .testTag(TestTag.Settings.SettingsScreen) - .statusBarsPadding() - ) { contentPadding -> - SettingsScreenContent( - contentPadding = contentPadding, - listState = listState, - onClickUnlockEgk = { unlockMethod -> - navController.navigate( - CardUnlockRoutes.CardUnlockIntroScreen.path( - unlockMethod = unlockMethod.name - ) - ) - }, - onClickOrderHealthCard = { - navController.navigate(MainNavigationScreens.OrderHealthCard.path()) - }, - onClickAccessibilitySettings = { - navController.navigate(SettingsNavigationScreens.SettingsAccessibilityScreen.path()) - }, - onClickProductImprovementSettings = { - navController.navigate(SettingsNavigationScreens.SettingsProductImprovementsScreen.path()) - }, - onClickDeviceSecuritySettings = { - navController.navigate(SettingsNavigationScreens.SettingsDeviceSecurityScreen.path()) - }, - onClickLegalNotice = { - navController.navigate(SettingsNavigationScreens.SettingsLegalNoticeScreen.path()) - }, - onClickDataProtection = { - navController.navigate(SettingsNavigationScreens.SettingsDataProtectionScreen.path()) - }, - onClickOpenSourceLicences = { - navController.navigate(SettingsNavigationScreens.SettingsOpenSourceLicencesScreen.path()) - }, - onClickAdditionalLicences = { - navController.navigate(SettingsNavigationScreens.SettingsAdditionalLicencesScreen.path()) - }, - onClickTermsOfUse = { - navController.navigate(SettingsNavigationScreens.SettingsTermsOfUseScreen.path()) - }, - onClickDebug = { - navController.navigate(MainNavigationScreens.Debug.path()) - }, - onClickEditProfile = { - navController.navigate(ProfileRoutes.ProfileScreen.path(profileId = it)) - }, - onClickSample = { - navController.navigate(PharmacyRoutes.subGraphName()) - }, - onClickDemoTracking = { - navController.navigate(TrackingScreenRoutes.subGraphName()) - }, - isDemomode = isDemomode, - localActivity = localActivity, - buildConfig = buildConfig, - profilesState = profilesState, - context = context - ) - } - } -} - -@Suppress("LongParameterList") -@Composable -private fun SettingsScreenContent( - contentPadding: PaddingValues, - listState: LazyListState, - onClickUnlockEgk: (UnlockMethod) -> Unit, - onClickOrderHealthCard: () -> Unit, - onClickAccessibilitySettings: () -> Unit, - onClickProductImprovementSettings: () -> Unit, - onClickDeviceSecuritySettings: () -> Unit, - onClickLegalNotice: () -> Unit, - onClickDataProtection: () -> Unit, - onClickOpenSourceLicences: () -> Unit, - onClickAdditionalLicences: () -> Unit, - onClickTermsOfUse: () -> Unit, - onClickDebug: () -> Unit, - onClickEditProfile: (ProfileIdentifier) -> Unit, - onClickSample: () -> Unit, - onClickDemoTracking: () -> Unit, - profilesState: List, - isDemomode: Boolean, - localActivity: ComponentActivity, - buildConfig: BuildConfigInformation, - context: Context -) { - val snackbar = LocalSnackbar.current - LazyColumn( - modifier = Modifier.testTag("settings_screen"), - contentPadding = contentPadding, - state = listState - ) { - @Requirement( - "O.Source_8", - "O.Source_9", - "O.Source_11", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Debug options are not accessible in the production version. All other debug mechanisms, including logging, are disabled in the build pipeline." // ktlint-disable max-line-length - ) - if (BuildConfigExtension.isInternalDebug) { - item { - DebugMenuSection(onClickDebug) - } - } - item { - ProfileSection(profilesState, onClickEditProfile) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Small) - ) - } - if (!isDemomode) { - item { - HealthCardSection( - onClickUnlockEgk = { unlockMethod -> - onClickUnlockEgk(unlockMethod) - }, - onClickOrderHealthCard = { - onClickOrderHealthCard() - } - ) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Small) - ) - } - } - item { - GlobalSettingsSection( - isDemomode = isDemomode, - onClickAccessibilitySettings = { - onClickAccessibilitySettings() - }, - onClickProductImprovementSettings = { - onClickProductImprovementSettings() - }, - onClickDeviceSecuritySettings = { - onClickDeviceSecuritySettings() - }, - onClickDemoModeEnd = { - DemoModeIntent.startAppWithNormalMode(localActivity) - }, - onClickDemoMode = { - DemoModeIntent.startAppWithDemoMode(localActivity) - } - ) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Small) - ) - } - item { - ContactSection( - darkMode = buildConfig.inDarkTheme(), - language = buildConfig.language(), - versionName = buildConfig.versionName(), - nfcInfo = buildConfig.nfcInformation(context), - phoneModel = buildConfig.model() - ) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Small) - ) - } - item { - LegalSection( - onClickLegalNotice, - onClickDataProtection, - onClickOpenSourceLicences, - onClickAdditionalLicences, - onClickTermsOfUse - ) - } - if (BuildConfigExtension.isInternalDebug) { - item { - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Small) - ) - } - item { - Text( - text = "Debug section", - style = AppTheme.typography.h6, - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium / 2, - top = PaddingDefaults.Medium - ) - ) - } - item { - LabelButton( - Icons.Outlined.TireRepair, - "Debug section", - modifier = Modifier.testTag("debug-section") - ) { - snackbar.show("TODO: Debug section comes here") - } - } - item { - LabelButton( - Icons.Outlined.SettingsInputComposite, - "Ui Components", - modifier = Modifier.testTag("ui-components") - ) { - onClickSample() - } - } - item { - LabelButton( - Icons.Outlined.ChecklistRtl, - "Tracking Debug", - modifier = Modifier.testTag("tracking-debug") - ) { - onClickDemoTracking() - } - } - } - item { - AboutSection( - modifier = Modifier.padding(top = 76.dp), - buildVersionName = buildConfig.versionName() - ) - } - } -} - -@Composable -private fun GlobalSettingsSection( - isDemomode: Boolean, - onClickAccessibilitySettings: () -> Unit, - onClickProductImprovementSettings: () -> Unit, - onClickDeviceSecuritySettings: () -> Unit, - onClickDemoModeEnd: () -> Unit, - onClickDemoMode: () -> Unit - -) { - Column { - Text( - text = stringResource(R.string.settings_personal_settings_header), - style = AppTheme.typography.h6, - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small, - top = PaddingDefaults.Medium - ) - ) - if (isDemomode) { - LabelButton( - icon = painterResource(R.drawable.magic_wand_filled), - stringResource(R.string.demo_mode_settings_end_title) - ) { - onClickDemoModeEnd() - } - } else { - LabelButton( - icon = painterResource(R.drawable.magic_wand_filled), - stringResource(R.string.demo_mode_settings_title) - ) { - onClickDemoMode() - } - } - LabelButton( - Icons.Outlined.AccessibilityNew, - stringResource(R.string.settings_accessibility_header) - ) { - onClickAccessibilitySettings() - } - LabelButton( - Icons.Outlined.Timeline, - stringResource(R.string.settings_product_improvement_header) - ) { - onClickProductImprovementSettings() - } - LabelButton( - Icons.Outlined.Security, - stringResource(R.string.settings_device_security_header) - ) { - onClickDeviceSecuritySettings() - } - } -} - -@Composable -private fun ProfileSection( - profiles: List, - onClickEditProfile: (ProfileIdentifier) -> Unit -) { - Column { - Text( - text = stringResource(R.string.settings_profiles_headline), - style = AppTheme.typography.h6, - modifier = Modifier - .padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small - ) - .testTag("Profiles") - ) - - profiles.forEach { profile -> - ProfileCard( - profile = profile, - onClickEdit = { onClickEditProfile(profile.id) } - ) - } - } - SpacerLarge() -} - -@Composable -private fun ProfileCard( - profile: Profile, - onClickEdit: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(role = Role.Button) { - onClickEdit() - } - .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.ShortMedium) - .testTag(TestTag.Settings.ProfileButton), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar( - modifier = Modifier.size(48.dp), - emptyIcon = Icons.Rounded.PersonOutline, - profile = profile, - ssoStatusColor = null, - iconModifier = Modifier.size(20.dp) - ) - SpacerMedium() - Text( - modifier = Modifier.weight(1f), - text = profile.name, - style = AppTheme.typography.body1 - ) - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun ProfileNameDialog( - initialProfileName: String = "", - profileController: ProfileController, - wantRemoveLastProfile: Boolean = false, - onEdit: (text: String) -> Unit, - onDismissRequest: () -> Unit -) { - val profiles by profileController.getProfilesState() - var textValue by remember { mutableStateOf(initialProfileName) } - var duplicated by remember { mutableStateOf(false) } - - val title = if (wantRemoveLastProfile) { - stringResource(R.string.profile_edit_name_for_default) - } else { - stringResource(R.string.profile_edit_name) - } - - val infoText = if (wantRemoveLastProfile) { - stringResource(R.string.profile_edit_name_for_default_info) - } else if (initialProfileName.isNotEmpty()) { - stringResource(R.string.profile_edit_name_for_rename_info) - } else { - stringResource(R.string.profile_edit_name_info) - } - - AlertDialog( - modifier = Modifier.testTag(TestTag.Settings.AddProfileDialog.Modal), - title = { - Text( - title, - style = AppTheme.typography.subtitle1 - ) - }, - properties = DialogProperties(dismissOnClickOutside = false), - onDismissRequest = onDismissRequest, - text = { - Column { - Text( - infoText, - style = AppTheme.typography.body2 - ) - Box(modifier = Modifier.padding(top = PaddingDefaults.ShortMedium)) { - OutlinedTextField( - modifier = Modifier.testTag(TestTag.Settings.AddProfileDialog.ProfileNameTextField), - value = textValue, - singleLine = true, - onValueChange = { - val isExistingProfileName = profiles.containsProfileWithName(textValue) - val isNotInitialProfileName = textValue.trim() != initialProfileName - textValue = it.trimStart().sanitizeProfileName() - duplicated = isNotInitialProfileName && isExistingProfileName && !wantRemoveLastProfile - }, - keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions { - if (!duplicated && textValue.isNotEmpty()) { - onEdit(textValue) - } - }, - placeholder = { Text(stringResource(R.string.profile_edit_name_place_holder)) }, - isError = duplicated - ) - } - if (duplicated) { - Text( - stringResource(R.string.edit_profile_duplicated_profile_name), - color = AppTheme.colors.red600, - style = AppTheme.typography.caption1, - modifier = Modifier.padding(start = PaddingDefaults.Medium) - ) - } - } - }, - buttons = { - TextButton( - modifier = Modifier.testTag(TestTag.Settings.AddProfileDialog.CancelButton), - onClick = { onDismissRequest() } - ) { - Text(stringResource(R.string.cancel).uppercase(Locale.getDefault())) - } - TextButton( - modifier = Modifier.testTag(TestTag.Settings.AddProfileDialog.ConfirmButton), - enabled = !duplicated && textValue.isNotEmpty(), - onClick = { - onEdit(textValue) - } - ) { - Text(stringResource(R.string.ok).uppercase(Locale.getDefault())) - } - } - ) - - val keyboardController = LocalSoftwareKeyboardController.current - DisposableEffect(Unit) { - onDispose { - keyboardController?.hide() - } - } -} - -@Composable -private fun HealthCardSection( - onClickUnlockEgk: (unlockMethod: UnlockMethod) -> Unit, - onClickOrderHealthCard: () -> Unit -) { - Column { - Text( - text = stringResource(R.string.health_card_section_header), - style = AppTheme.typography.h6, - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small, - top = PaddingDefaults.Medium - ) - ) - - LabelButton( - modifier = Modifier.testTag(TestTag.Settings.OrderNewCardButton), - icon = painterResource(R.drawable.ic_order_egk), - text = stringResource(R.string.health_card_section_order_card) - ) { - onClickOrderHealthCard() - } - - LabelButton( - Icons.Outlined.HelpOutline, - stringResource(R.string.health_card_section_unlock_card_forgot_pin) - ) { - onClickUnlockEgk(UnlockMethod.ResetRetryCounterWithNewSecret) - } - - LabelButton( - painterResource(R.drawable.ic_reset_pin), - stringResource(R.string.health_card_section_unlock_card_reset_pin) - ) { - onClickUnlockEgk(UnlockMethod.ChangeReferenceData) - } - - LabelButton( - Icons.Outlined.LockOpen, - stringResource(R.string.health_card_section_unlock_card_no_reset) - ) { - onClickUnlockEgk(UnlockMethod.ResetRetryCounter) - } - } -} - -@Composable -private fun DebugMenuSection(onClickDebug: () -> Unit) { - OutlinedDebugButton( - text = stringResource(id = R.string.debug_menu), - onClick = { onClickDebug() }, - modifier = Modifier - .fillMaxWidth() - .padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small, - top = PaddingDefaults.Medium - ) - .testTag(TestTag.Settings.DebugMenuButton) - ) -} - -@Composable -private fun LegalSection( - onClickLegalNotice: () -> Unit, - onClickDataProtection: () -> Unit, - onClickOpenSourceLicences: () -> Unit, - onClickAdditionalLicences: () -> Unit, - onClickTermsOfUse: () -> Unit -) { - Column { - Text( - text = stringResource(R.string.settings_legal_headline), - style = AppTheme.typography.h6, - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small, - top = PaddingDefaults.Medium - ) - ) - LabelButton( - Icons.Outlined.Info, - stringResource(R.string.settings_legal_imprint), - modifier = Modifier.testTag("settings/imprint") - ) { - onClickLegalNotice() - } - @Requirement( - "O.Arch_9", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Display data protection within settings" - ) - LabelButton( - Icons.Outlined.PrivacyTip, - stringResource(R.string.settings_legal_dataprotection), - modifier = Modifier.testTag("settings/privacy") - ) { - onClickDataProtection() - } - LabelButton( - Icons.Outlined.Wysiwyg, - stringResource(R.string.settings_legal_tos), - modifier = Modifier.testTag("settings/tos") - ) { - onClickTermsOfUse() - } - LabelButton( - Icons.Outlined.Code, - stringResource(R.string.settings_legal_licences), - modifier = Modifier.testTag("settings/licences") - ) { - onClickOpenSourceLicences() - } - LabelButton( - Icons.Outlined.Source, - stringResource(R.string.settings_licence_pharmacy_search), - modifier = Modifier.testTag("settings/additional_licences") - ) { - onClickAdditionalLicences() - } - } -} - -@Composable -private fun LabelButton( - icon: ImageVector, - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Icon(icon, null, tint = AppTheme.colors.primary600) - SpacerMedium() - Text( - modifier = Modifier.weight(1f), - text = text, - style = AppTheme.typography.body1 - ) - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } -} - -@Composable -fun LabelButton( - icon: Painter, - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(PaddingDefaults.Medium) - .semantics(mergeDescendants = true) {} - ) { - Image(painter = icon, contentDescription = null) - SpacerMedium() - Text( - modifier = Modifier.weight(1f), - text = text, - style = AppTheme.typography.body1 - ) - Icon(Icons.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } -} - -@Composable -private fun AboutSection( - modifier: Modifier, - buildVersionName: String -) { - Column( - modifier = modifier - .padding(PaddingDefaults.Medium) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CompositionLocalProvider( - LocalTextStyle provides AppTheme.typography.body2, - LocalContentColor provides AppTheme.colors.neutral600 - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Rounded.PhoneAndroid, null, modifier = Modifier.size(16.dp)) - SpacerTiny() - Text( - stringResource(R.string.about_version, buildVersionName) - ) - } - SpacerTiny() - Text( - stringResource(R.string.about_buildhash, BuildKonfig.GIT_HASH) - ) - } - } -} - -@Composable -private fun ContactSection( - darkMode: String, - versionName: String, - language: String, - phoneModel: String, - nfcInfo: String -) { - val context = LocalContext.current - val contactHeader = stringResource(R.string.settings_contact_headline) - - Column { - val phoneNumber = stringResource(R.string.settings_contact_hotline_number) - val mailAddress = stringResource(R.string.settings_contact_mail_address) - val subject = stringResource(R.string.settings_feedback_mail_subject) - val body = buildFeedbackBodyWithDeviceInfo( - darkMode = darkMode, - versionName = versionName, - language = language, - phoneModel = phoneModel, - nfcInfo = nfcInfo - ) - SpacerMedium() - Text( - text = contactHeader, - style = AppTheme.typography.h6, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - SpacerSmall() - LabelButton( - icon = Icons.Outlined.Mail, - text = stringResource(R.string.settings_contact_feedback_form), - onClick = { - openMailClient(context, mailAddress, body, subject) - } - ) - LabelButton( - icon = Icons.Rounded.Phone, - text = stringResource(R.string.settings_contact_hotline), - onClick = { context.handleIntent(providePhoneIntent(phoneNumber)) } - ) - Text( - text = stringResource(R.string.settings_contact_technical_support_description), - style = AppTheme.typography.body2l, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreen.kt deleted file mode 100644 index b2e73dac..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreen.kt +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.settings.presentation.rememberPasswordSettingsController -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.BottomAppBar -import de.gematik.ti.erp.app.utils.compose.ConfirmationPasswordTextField -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PasswordStrength -import de.gematik.ti.erp.app.utils.compose.PasswordTextField -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.validatePasswordScore - -class SettingsSetAppPasswordScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - var password by remember { mutableStateOf("") } - var repeatedPassword by remember { mutableStateOf("") } - var passwordScore by remember { mutableIntStateOf(0) } - val focusRequester = FocusRequester.Default - val settingsController = rememberPasswordSettingsController() - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - topBarTitle = stringResource(R.string.settings_password_headline), - navigationMode = NavigationBarMode.Back, - listState = listState, - onBack = navController::popBackStack, - bottomBar = { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(modifier = Modifier.weight(1f)) - Button( - onClick = { - settingsController.selectPasswordAsAuthenticationMode(password) - navController.popBackStack() - }, - enabled = validatePassword( - password = password, - repeatedPassword = repeatedPassword, - score = passwordScore - ), - shape = RoundedCornerShape(PaddingDefaults.Small) - ) { - Text(stringResource(R.string.settings_password_save)) - } - SpacerMedium() - } - } - ) { innerPadding -> - SettingsSetAppPasswordScreenContent( - innerPadding, - listState, - focusRequester, - password, - repeatedPassword, - passwordScore, - onAuthenticateWithPassword = { - settingsController.selectPasswordAsAuthenticationMode(password = it) - navController.popBackStack() - }, - onPasswortChange = { - repeatedPassword = "" - password = it - }, - onRepeatedPasswortChange = { - repeatedPassword = it - }, - onScoreChange = { - passwordScore = it - } - ) - } - } -} - -@Composable -private fun SettingsSetAppPasswordScreenContent( - innerPadding: PaddingValues, - listState: LazyListState, - focusRequester: FocusRequester, - password: String, - repeatedPassword: String, - passwordScore: Int, - onAuthenticateWithPassword: (String) -> Unit, - onPasswortChange: (String) -> Unit, - onRepeatedPasswortChange: (String) -> Unit, - onScoreChange: (Int) -> Unit -) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .padding(PaddingDefaults.Medium), - contentPadding = innerPadding, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) - ) { - item { - PasswordTextField( - modifier = Modifier.fillMaxWidth(), - value = password, - onValueChange = onPasswortChange, - allowAutofill = true, - allowVisiblePassword = true, - label = { - Text(stringResource(R.string.settings_password_enter)) - }, - onSubmit = { focusRequester.requestFocus() } - ) - SpacerTiny() - PasswordStrength( - modifier = Modifier.fillMaxWidth(), - password = password, - onScoreChange = onScoreChange - ) - } - item { - ConfirmationPasswordTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - password = password, - passwordScore = passwordScore, - value = repeatedPassword, - onValueChange = onRepeatedPasswortChange, - onSubmit = { - if ( - validatePassword( - password = password, - repeatedPassword = repeatedPassword, - score = passwordScore - ) - ) { - onAuthenticateWithPassword(password) - } - } - ) - } - } -} - -fun validatePassword(password: String, repeatedPassword: String, score: Int): Boolean = - password.isNotBlank() && password == repeatedPassword && validatePasswordScore(score) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsTermsOfUseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsTermsOfUseScreen.kt deleted file mode 100644 index 0c52ad0a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsTermsOfUseScreen.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE -import de.gematik.ti.erp.app.webview.WebViewScreen - -@Requirement( - "O.Purp_1#1", - "O.Arch_8#5", - "O.Plat_11#5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Display terms of use as part of the settings. " + - "Webview containing local html without javascript" -) -@Requirement( - "O.Arch_8#3", - "O.Plat_11#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Webview containing local html without javascript" -) -class SettingsTermsOfUseScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - WebViewScreen( - title = stringResource(R.string.onb_terms_of_use), - onBack = navController::popBackStack, - url = URI_TERMS_OF_USE - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/AllowScreenshotsDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/AllowScreenshotsDialog.kt new file mode 100644 index 00000000..c5a34cb9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/AllowScreenshotsDialog.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun AllowScreenshotDialogWithListener( + event: ComposableEvent, + dialog: DialogScaffold, + onAllowScreenshots: () -> Unit +) { + event.listen { + dialog.show { + AllowScreenshotDialog( + onDismissRequest = { + it.dismiss() + }, + onConfirmRequest = { + onAllowScreenshots() + it.dismiss() + } + ) + } + } +} + +@Composable +fun AllowScreenshotDialog( + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.allow_screenshots_alert_title), + bodyText = stringResource(R.string.allow_screenshots_alert_body), + confirmText = stringResource(R.string.allow_screenshots_alert_confirm), + dismissText = stringResource(R.string.allow_screenshots_alert_dismiss), + onConfirmRequest = onConfirmRequest, + onDismissRequest = onDismissRequest + ) +} + +@LightDarkPreview +@Composable +fun UniversalLinkWrongDialogPreview() { + PreviewAppTheme { + AllowScreenshotDialog( + onConfirmRequest = {}, + onDismissRequest = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/GlobalSettingsSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/GlobalSettingsSection.kt new file mode 100644 index 00000000..c1ebd449 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/GlobalSettingsSection.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Medication +import androidx.compose.material.icons.outlined.Security +import androidx.compose.material.icons.outlined.Timeline +import androidx.compose.material.icons.rounded.Camera +import androidx.compose.material.icons.rounded.Language +import androidx.compose.material.icons.rounded.ZoomIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.model.SettingsActions +import de.gematik.ti.erp.app.settings.presentation.SettingStatesData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.LabelButton +import de.gematik.ti.erp.app.utils.compose.LabeledSwitch + +@Suppress("LongParameterList", "FunctionNaming") +@Composable +fun GlobalSettingsSection( + isDemoMode: Boolean, + zoomState: State, + screenShotState: State, + isMedicationPlanEnabled: Boolean, + onEnableZoom: () -> Unit, + onDisableZoom: () -> Unit, + onAllowScreenshots: () -> Unit, + onDisallowScreenshots: () -> Unit, + settingsActions: SettingsActions, + onClickMedicationPlan: () -> Unit +) { + Column { + Text( + text = stringResource(R.string.settings_personal_settings_header), + style = AppTheme.typography.h6, + modifier = Modifier.padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Small, + top = PaddingDefaults.Medium + ) + ) + + if (isMedicationPlanEnabled) { + LabelButton( + Icons.Outlined.Medication, + stringResource(R.string.medication_plan_settings_title) + ) { + onClickMedicationPlan() + } + } + LabelButton(icon = Icons.Rounded.Language, text = stringResource(R.string.settings_language_label)) { + settingsActions.onClickLanguageSettings() + } + LabelButton( + Icons.Outlined.Timeline, + stringResource(R.string.settings_product_improvement_header) + ) { + settingsActions.onClickProductImprovementSettings() + } + LabelButton( + Icons.Outlined.Security, + stringResource(R.string.settings_app_security_header) + ) { + settingsActions.onClickDeviceSecuritySettings() + } + + ZoomSection(zoomChecked = zoomState.value.zoomEnabled) { zoomEnabled -> + if (zoomEnabled) onEnableZoom() else onDisableZoom() + } + + AllowScreenShotsSection( + screenShotsAllowed = screenShotState.value, + onAllowScreenshots = onAllowScreenshots, + onDisallowScreenshots = onDisallowScreenshots + ) + + if (isDemoMode) { + LabelButton( + icon = painterResource(R.drawable.magic_wand_filled), + stringResource(R.string.demo_mode_settings_end_title) + ) { + settingsActions.onClickDemoModeEnd() + } + } else { + LabelButton( + icon = painterResource(R.drawable.magic_wand_filled), + stringResource(R.string.demo_mode_settings_title) + ) { + settingsActions.onClickDemoMode() + } + } + } +} + +@Composable +private fun ZoomSection( + modifier: Modifier = Modifier, + zoomChecked: Boolean, + onZoomChange: (Boolean) -> Unit +) { + LabeledSwitch( + modifier = modifier, + checked = zoomChecked, + onCheckedChange = onZoomChange, + icon = Icons.Rounded.ZoomIn, + header = stringResource(R.string.settings_accessibility_zoom_toggle) + ) +} + +@Composable +private fun AllowScreenShotsSection( + screenShotsAllowed: Boolean, + onAllowScreenshots: () -> Unit, + onDisallowScreenshots: () -> Unit +) { + LabeledSwitch( + checked = screenShotsAllowed, + onCheckedChange = { checked -> + if (checked) onAllowScreenshots() else onDisallowScreenshots() + }, + icon = Icons.Rounded.Camera, + header = stringResource(R.string.settings_screenshots_toggle_text) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/OrganDonationRegisterDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/OrganDonationRegisterDialog.kt new file mode 100644 index 00000000..e14c1a5c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/components/OrganDonationRegisterDialog.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@Composable +fun OrganDonationRegisterDialog( + event: ComposableEvent, + dialog: DialogScaffold, + createIntent: () -> Unit +) { + event.listen { + dialog.show { + OrganDonationRegisterDialog_( + onDismissRequest = { + it.dismiss() + }, + onConfirmRequest = { + createIntent() + it.dismiss() + } + ) + } + } +} + +@Composable +private fun OrganDonationRegisterDialog_( + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit +) { + ErezeptAlertDialog( + title = stringResource(R.string.organ_donation_dialog_title), + bodyText = stringResource(R.string.organ_donation_dialog_info), + confirmText = stringResource(R.string.organ_donation_dialog_confirm), + dismissText = stringResource(R.string.organ_donation_dialog_dismiss), + onConfirmRequest = onConfirmRequest, + onDismissRequest = onDismissRequest + ) +} + +@LightDarkPreview +@Composable +fun OrganDonationRegisterDialogPreview() { + PreviewAppTheme { + OrganDonationRegisterDialog_( + onConfirmRequest = {}, + onDismissRequest = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/AppSecuritySettingsParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/AppSecuritySettingsParameterProvider.kt new file mode 100644 index 00000000..fd9263ae --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/AppSecuritySettingsParameterProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.userauthentication.ui.preview.AuthMethodBiometry +import de.gematik.ti.erp.app.userauthentication.ui.preview.AuthMethodBoth +import de.gematik.ti.erp.app.userauthentication.ui.preview.AuthMethodPassword + +data class AppSecuritySettingsParameter( + val name: String, + val authentication: SettingsData.Authentication +) + +class AppSecuritySettingsParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AppSecuritySettingsParameter( + name = "Biometry", + authentication = AuthMethodBiometry + ), + AppSecuritySettingsParameter( + name = "Password", + authentication = AuthMethodPassword + ), + AppSecuritySettingsParameter( + name = "Both", + authentication = AuthMethodBoth + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SetAppPasswordParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SetAppPasswordParameterProvider.kt new file mode 100644 index 00000000..8dd16a7b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SetAppPasswordParameterProvider.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.PasswordEvaluation +import de.gematik.ti.erp.app.utils.compose.PasswordScore +import de.gematik.ti.erp.app.utils.compose.presentation.PasswordFieldsData + +data class SetAppPasswordParameter( + val name: String, + val passwordFieldsState: PasswordFieldsData +) + +class SetAppPasswordParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SetAppPasswordParameter( + name = "Weak", + passwordFieldsState = PasswordFieldsData( + password = "password", + repeatedPassword = "password", + passwordEvaluation = PasswordEvaluation( + PasswordScore.Weak, + "password is weak" + ), + passwordIsValidAndConsistent = false, + repeatedPasswordHasError = false + ) + ), + SetAppPasswordParameter( + name = "Strong", + passwordFieldsState = PasswordFieldsData( + password = "password123", + repeatedPassword = "password123", + passwordEvaluation = PasswordEvaluation( + PasswordScore.Strong, + "password123 is strong" + ), + passwordIsValidAndConsistent = true, + repeatedPasswordHasError = false + ) + ), + SetAppPasswordParameter( + name = "VeryStrong", + passwordFieldsState = PasswordFieldsData( + password = "passwordIsVERYStrong", + repeatedPassword = "passwordIsVERYStrong", + passwordEvaluation = PasswordEvaluation( + PasswordScore.VeryStrong, + "password is very Strong" + ), + passwordIsValidAndConsistent = true, + repeatedPasswordHasError = false + ) + ), + SetAppPasswordParameter( + name = "InConsistent", + passwordFieldsState = PasswordFieldsData( + password = "1234", + repeatedPassword = "0123", + passwordEvaluation = PasswordEvaluation( + PasswordScore.VeryWeak, + "password is very weak" + ), + passwordIsValidAndConsistent = false, + repeatedPasswordHasError = true + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsProductImprovementPreviewParameter.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsProductImprovementPreviewParameter.kt new file mode 100644 index 00000000..71b78ca1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsProductImprovementPreviewParameter.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +data class AnalyticsAllowPreviewData( + val name: String, + var isAnalyticsAllowed: Boolean +) + +class SettingsProductImprovementPreviewParameter : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + AnalyticsAllowPreviewData( + name = "AnalyticsAllowed", + isAnalyticsAllowed = true + ), + AnalyticsAllowPreviewData( + name = "AnalyticsNotAllowed", + isAnalyticsAllowed = false + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsScreenPreviewData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsScreenPreviewData.kt new file mode 100644 index 00000000..60dcc299 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/preview/SettingsScreenPreviewData.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.preview + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.profiles.presentation.ProfileController.Companion.DEFAULT_EMPTY_PROFILE +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.settings.presentation.SettingStatesData + +val LocalIsPreviewMode = compositionLocalOf { false } + +data class SettingsScreenPreviewData( + val name: String, + val profiles: List, + val buildConfig: BuildConfigInformation, + val zoomState: MutableState, + val screenShotsState: MutableState +) + +object MockBuildConfigInformation : BuildConfigInformation { + override fun versionName(): String = "1.25.0-RC2-debug" + override fun versionCode(): String = "3598" + override fun model(): String = "samsung SM-S921B (e1sxeea)" + override fun language(): String = "en" + + @Composable + override fun inDarkTheme(): String = "an" + override fun nfcInformation(context: Context): String = "available" + override fun isMockedApp(): Boolean = false +} + +class SettingsScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SettingsScreenPreviewData( + name = "SettingsScreen", + profiles = listOf( + DEFAULT_EMPTY_PROFILE.copy(name = "Max Mustermann") + ), + buildConfig = MockBuildConfigInformation, + zoomState = mutableStateOf(SettingStatesData.defaultZoomState), + screenShotsState = mutableStateOf(false) + ), + SettingsScreenPreviewData( + name = "SettingsScreen", + profiles = listOf( + DEFAULT_EMPTY_PROFILE + ), + buildConfig = MockBuildConfigInformation, + zoomState = mutableStateOf(SettingStatesData.ZoomState(zoomEnabled = true)), + screenShotsState = mutableStateOf(true) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAdditionalLicencesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAdditionalLicencesScreen.kt new file mode 100644 index 00000000..5642fad5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAdditionalLicencesScreen.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid + +class SettingsAdditionalLicencesScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + AdditionalLicensesScreenScaffoldContent( + onBack = navController::popBackStack + ) + } +} + +@Composable +fun AdditionalLicensesScreenScaffoldContent( + onBack: () -> Unit = {} +) { + val listState = rememberLazyListState() + + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.settings_licence_pharmacy_search), + navigationMode = NavigationBarMode.Close, + onBack = onBack, + listState = listState + ) { + SettingsAdditionalLicencesScreenContent( + listState + ) + } +} + +@Composable +fun SettingsAdditionalLicencesScreenContent( + listState: LazyListState +) { + LazyColumn( + modifier = Modifier + .padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + top = PaddingDefaults.Medium, + bottom = (PaddingDefaults.XLarge * 2) + ), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + item { + Text( + stringResource(R.string.license_pharmacy_search_description), + style = AppTheme.typography.body1, + color = AppTheme.colors.neutral999 + ) + } + item { + val link = + provideLinkForString( + stringResource(id = R.string.license_pharmacy_search_web_link), + annotation = stringResource(id = R.string.license_pharmacy_search_web_link), + tag = "URL", + linkColor = AppTheme.colors.primary500 + ) + + val uriHandler = LocalUriHandler.current + + ClickableTaggedText( + annotatedStringResource(R.string.license_pharmacy_search_web_link_info, link), + style = AppTheme.typography.body1, + onClick = { range -> + uriHandler.openUriWhenValid(range.item) + } + ) + } + } +} + +@LightDarkPreview +@Composable +fun AdditionalLicensesScreenScaffoldScreenPreview() { + PreviewAppTheme { + AdditionalLicensesScreenScaffoldContent( + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAllowAnalyticsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAllowAnalyticsScreen.kt new file mode 100644 index 00000000..9f3142d9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAllowAnalyticsScreen.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar +import de.gematik.ti.erp.app.settings.presentation.rememberAnalyticsSettingsController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.annotatedStringBold +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.shortToast + +@Requirement( + "A_19091-01#5", + "A_19092-01#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Display opt-in for analytics. Full app functionality is available also without opting in." +) +class SettingsAllowAnalyticsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val settingsController = rememberAnalyticsSettingsController() + val context = LocalContext.current + val allowStars = stringResource(R.string.settings_tracking_allow_emoji) + val allowText = annotatedStringResource( + R.string.settings_tracking_allow_info, + annotatedStringBold(allowStars) + ).toString() + val disAllowToast = stringResource(R.string.settings_tracking_disallow_info) + val topBarTitle = stringResource(R.string.settings_tracking_allow_title) + + SettingsAllowAnalyticsScaffoldContent( + allowText = allowText, + disAllowToast = disAllowToast, + topBarTitle = topBarTitle, + onBack = { + settingsController.changeAnalyticsAllowedState(false) + context.shortToast(disAllowToast) + navController.popBackStack() + }, + onAllowClick = { + settingsController.changeAnalyticsAllowedState(true) + context.shortToast(allowText) + navController.popBackStack() + } + ) + } +} + +@Composable +fun SettingsAllowAnalyticsScaffoldContent( + allowText: String, + disAllowToast: String, + topBarTitle: String, + onBack: () -> Unit, + onAllowClick: () -> Unit, + contentPadding: PaddingValues = PaddingValues() +) { + val lazyListState = rememberLazyListState() + val context = LocalContext.current + AnimatedElevationScaffold( + modifier = Modifier.navigationBarsPadding(), + navigationMode = NavigationBarMode.Back, + topBarTitle = topBarTitle, + onBack = { + context.shortToast(disAllowToast) + onBack() + }, + listState = lazyListState, + bottomBar = { + @Requirement( + "A_19091-01#4", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "User confirms the opt in" + ) + OnboardingBottomBar( + buttonText = stringResource(R.string.settings_tracking_allow_button), + onButtonClick = { + context.shortToast(allowText) + onAllowClick() + }, + buttonEnabled = true, + info = null, + buttonModifier = Modifier.testTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) + ) + } + ) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .wrapContentSize() + .padding(horizontal = PaddingDefaults.Medium) + .padding(bottom = contentPadding.calculateBottomPadding()) + .testTag(TestTag.Onboarding.Analytics.ScreenContent) + ) { + item { + @Requirement( + "A_19090-01#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Display explanation of data processing for analytics opt-in" + ) + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .semantics(mergeDescendants = true) {} + ) { + Text( + stringResource(R.string.settings_tracking_dialog_title), + style = AppTheme.typography.h6, + modifier = Modifier.padding( + top = PaddingDefaults.Medium, + bottom = PaddingDefaults.Large + ) + ) + Text( + stringResource(R.string.settings_tracking_dialog_text_1), + style = AppTheme.typography.body1, + modifier = Modifier.padding(bottom = PaddingDefaults.Small) + ) + Text( + stringResource(R.string.settings_tracking_dialog_text_2), + style = AppTheme.typography.body1, + modifier = Modifier.padding(bottom = PaddingDefaults.Small) + ) + Text( + stringResource(R.string.settings_tracking_dialog_text_3), + style = AppTheme.typography.body1, + modifier = Modifier.padding(bottom = PaddingDefaults.Medium) + ) + } + } + } + } +} + +@LightDarkPreview +@Composable +fun SettingsAllowAnalyticsScaffoldContentPreview() { + PreviewAppTheme { + SettingsAllowAnalyticsScaffoldContent( + allowText = stringResource(R.string.settings_tracking_allow_emoji), + disAllowToast = stringResource(R.string.settings_tracking_disallow_info), + topBarTitle = stringResource(R.string.settings_tracking_allow_title), + onBack = {}, + onAllowClick = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAppSecurityScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAppSecurityScreen.kt new file mode 100644 index 00000000..b163b1b6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsAppSecurityScreen.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.authentication.ui.components.EnrollBiometricDialog +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens +import de.gematik.ti.erp.app.settings.presentation.rememberAppSecuritySettingsController +import de.gematik.ti.erp.app.settings.ui.preview.AppSecuritySettingsParameter +import de.gematik.ti.erp.app.settings.ui.preview.AppSecuritySettingsParameterProvider +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SwitchRightWithText +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class SettingsAppSecurityScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val deviceSecuritySettingsController = rememberAppSecuritySettingsController() + val authenticationState by deviceSecuritySettingsController.authenticationState.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + + with(deviceSecuritySettingsController.events) { + OpenPasswordScreenEventListener( + openPasswordScreenEvent = openPasswordScreenEvent, + onNavigateToPasswordScreen = { + navController.navigate(SettingsNavigationScreens.SettingsSetAppPasswordScreen.path()) + } + ) + EnrollBiometricDialog( + context = context, + dialog = dialog, + event = enrollBiometryEvent + ) + } + + BackHandler { + navController.popBackStack() + } + + SettingsAppSecurityScreenScaffold( + listState = listState, + authenticationState = authenticationState, + onSwitchDeviceSecurityAuthentication = deviceSecuritySettingsController::onSwitchDeviceSecurityAuthentication, + onSwitchPasswordAuthentication = deviceSecuritySettingsController::onSwitchPasswordAuthentication, + onOpenPasswordScreen = { + deviceSecuritySettingsController.events.openPasswordScreenEvent.trigger(Unit) + }, + onBack = { + navController.popBackStack() + } + ) + } +} + +@Composable +private fun SettingsAppSecurityScreenScaffold( + listState: LazyListState, + authenticationState: SettingsData.Authentication, + onSwitchDeviceSecurityAuthentication: (Boolean) -> Unit, + onSwitchPasswordAuthentication: (Boolean) -> Unit, + onOpenPasswordScreen: () -> Unit, + onBack: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.settings_app_security_header), + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = onBack + ) { contentPadding -> + SettingsAppSecurityScreenContent( + contentPadding, + listState, + authentication = authenticationState, + onNavigateToPasswordScreen = onOpenPasswordScreen, + onSwitchDeviceSecurityAuthentication = onSwitchDeviceSecurityAuthentication, + onSwitchPasswordAuthentication = onSwitchPasswordAuthentication + ) + } +} + +@Composable +private fun SettingsAppSecurityScreenContent( + contentPadding: PaddingValues, + listState: LazyListState, + authentication: SettingsData.Authentication, + onNavigateToPasswordScreen: () -> Unit, + onSwitchDeviceSecurityAuthentication: (Boolean) -> Unit, + onSwitchPasswordAuthentication: (Boolean) -> Unit +) { + LazyColumn( + contentPadding = contentPadding, + state = listState + ) { + item { + Text( + text = stringResource(R.string.settings_app_security_info), + style = AppTheme.typography.body1l, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) + } + item { + DeviceSecuritySwitch( + authentication = authentication, + onSwitchDeviceSecurityAuthentication = onSwitchDeviceSecurityAuthentication + ) + } + item { + PasswordSwitch( + authentication = authentication, + onSwitchPasswordAuthentication = onSwitchPasswordAuthentication + ) + } + item { + if (authentication.passwordIsSet) { + ChangePasswordSection( + text = stringResource(id = R.string.settings_app_security_change_password), + onOpenPasswordScreen = onNavigateToPasswordScreen + ) + } + } + } +} + +@Composable +private fun DeviceSecuritySwitch( + authentication: SettingsData.Authentication, + onSwitchDeviceSecurityAuthentication: (Boolean) -> Unit +) { + SwitchRightWithText( + text = stringResource(id = R.string.settings_app_security_device_security), + checked = authentication.deviceSecurity, + onCheckedChange = { onSwitchDeviceSecurityAuthentication(it) }, + enabled = !authentication.methodIsDeviceSecurity // enabled only if device security is not the only method + ) +} + +@Composable +private fun PasswordSwitch( + authentication: SettingsData.Authentication, + onSwitchPasswordAuthentication: (Boolean) -> Unit +) { + SwitchRightWithText( + text = stringResource(id = R.string.settings_app_security_password), + checked = authentication.passwordIsSet, + onCheckedChange = { onSwitchPasswordAuthentication(it) }, + enabled = !authentication.methodIsPassword // enabled only if password is not the only method + ) +} + +@Composable +private fun ChangePasswordSection( + modifier: Modifier = Modifier, + text: String, + onOpenPasswordScreen: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(SizeDefaults.double)) + .clickable { + onOpenPasswordScreen() + } + .padding(PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + style = AppTheme.typography.body1, + text = text + ) + Icon( + imageVector = Icons.Rounded.ChevronRight, + contentDescription = null, + tint = AppTheme.colors.primary600 + ) + } +} + +@Composable +private fun OpenPasswordScreenEventListener( + openPasswordScreenEvent: ComposableEvent, + onNavigateToPasswordScreen: () -> Unit +) { + openPasswordScreenEvent.listen { + onNavigateToPasswordScreen() + } +} + +@LightDarkPreview +@Composable +fun SettingsAppSecurityScreenScaffoldPreview( + @PreviewParameter(AppSecuritySettingsParameterProvider::class) previewData: AppSecuritySettingsParameter +) { + val listState = rememberLazyListState() + PreviewAppTheme { + SettingsAppSecurityScreenScaffold( + listState = listState, + authenticationState = previewData.authentication, + onSwitchDeviceSecurityAuthentication = {}, + onSwitchPasswordAuthentication = {}, + onOpenPasswordScreen = {}, + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsDataProtectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsDataProtectionScreen.kt new file mode 100644 index 00000000..0ce2d495 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsDataProtectionScreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.utils.extensions.getUriDataTerms +import de.gematik.ti.erp.app.webview.WebViewScreen + +@Requirement( + "O.Purp_1#4", + "O.Arch_9#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display data privacy as part of the settings screen." +) +class SettingsDataProtectionScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Requirement( + "O.Arch_8#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Webview containing local html without javascript" + ) + @Composable + override fun Content() { + WebViewScreen( + title = stringResource(R.string.onb_data_consent), + onBack = navController::popBackStack, + url = getUriDataTerms() + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLanguageScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLanguageScreen.kt new file mode 100644 index 00000000..5a60f4c8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLanguageScreen.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.Divider +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.core.os.LocaleListCompat +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.settings.presentation.LanguageCode +import de.gematik.ti.erp.app.settings.presentation.mapLanguageCodeToName +import de.gematik.ti.erp.app.settings.presentation.rememberSettingsLanguageScreenController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.LanguageCodePreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.preview.TestScaffold +import java.util.Locale + +class SettingsLanguageScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val controller = rememberSettingsLanguageScreenController() + val languages = controller.languageList + + val lazyListState = rememberLazyListState() + + val selectedLanguage = remember { + val selectedAppLanguage = AppCompatDelegate.getApplicationLocales().toLanguageTags() + selectedAppLanguage.ifEmpty { + Locale.getDefault().language + } + } + + AnimatedElevationScaffold( + modifier = Modifier.navigationBarsPadding(), + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(R.string.language_selection_title), + onBack = navController::popBackStack, + listState = lazyListState + ) { + SettingsLanguageScreenContent(lazyListState, languages, selectedLanguage) + } + } +} + +@Composable +private fun SettingsLanguageScreenContent( + lazyListState: LazyListState, + languages: List, + selectedLanguage: String? +) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .wrapContentSize() + .padding( + horizontal = PaddingDefaults.Medium + ) + .testTag(TestTag.Settings.LanguageColumnList) + ) { + item { + SpacerMedium() + } + + itemsIndexed(languages) { index, languageCode -> + mapLanguageCodeToName(code = languageCode)?.let { + LanguageSelectionItem( + isFirstItem = index == 0, + language = it, + checked = languageCode == selectedLanguage || + (languageCode == "iw" && selectedLanguage == "he"), + onCheckedChange = { checked -> + if (checked) { + AppCompatDelegate.setApplicationLocales( + LocaleListCompat.forLanguageTags( + languageCode + ) + ) + } + } + ) + } + } + } +} + +@Composable +private fun LanguageSelectionItem( + isFirstItem: Boolean, + language: String, + standardText: String = stringResource(R.string.language_selection_is_standard), + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = checked, + onValueChange = onCheckedChange, + role = Role.RadioButton + ) + ) { + RadioButton( + selected = checked, + colors = RadioButtonDefaults.colors( + selectedColor = AppTheme.colors.primary600, + unselectedColor = AppTheme.colors.primary600 + ), + onClick = { + onCheckedChange(!checked) + } + + ) + Text( + language, + style = AppTheme.typography.body1 + ) + } + if (isFirstItem) { + Text( + modifier = Modifier + .padding(start = SizeDefaults.eightfoldAndHalf) + .offset(y = (-SizeDefaults.one)), + text = standardText, + style = AppTheme.typography.body2l + ) + Divider( + color = AppTheme.colors.neutral300, + modifier = Modifier + .padding(PaddingDefaults.Medium) + + ) + } + } +} + +@LightDarkPreview +@Composable +fun SettingsLanguageScreenScaffoldPreview( + @PreviewParameter(LanguageCodePreviewParameterProvider::class) selectedLanguage: String +) { + PreviewAppTheme { + val listState = rememberLazyListState() + + TestScaffold( + topBarTitle = stringResource(R.string.language_selection_title), + navigationMode = NavigationBarMode.Back + ) { + SettingsLanguageScreenContent(listState, LanguageCode.codes, selectedLanguage) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLegalNoticeScreen.kt similarity index 81% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLegalNoticeScreen.kt index 7177a4de..1a9b008d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsLegalNoticeScreen.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.settings.ui +package de.gematik.ti.erp.app.settings.ui.screens import android.content.Context import androidx.compose.foundation.Image @@ -54,16 +54,19 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.navigation.Screen import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerXLarge import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerXLarge import de.gematik.ti.erp.app.utils.compose.canHandleIntent import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.compose.provideEmailIntent import de.gematik.ti.erp.app.utils.compose.providePhoneIntent import de.gematik.ti.erp.app.utils.compose.shortToast +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid class SettingsLegalNoticeScreen( override val navController: NavController, @@ -71,19 +74,28 @@ class SettingsLegalNoticeScreen( ) : Screen() { @Composable override fun Content() { - val listState = rememberLazyListState() - - AnimatedElevationScaffold( - listState = listState, - navigationMode = NavigationBarMode.Back, - topBarTitle = stringResource(id = R.string.legal_notice_menu), + LegalNoticeScreenScaffoldContent( onBack = navController::popBackStack - ) { innerPadding -> - LegalNoticeScreenContent( - innerPadding, - listState - ) - } + ) + } +} + +@Composable +fun LegalNoticeScreenScaffoldContent( + onBack: () -> Unit = {} +) { + val listState = rememberLazyListState() + + AnimatedElevationScaffold( + listState = listState, + navigationMode = NavigationBarMode.Back, + topBarTitle = stringResource(id = R.string.legal_notice_menu), + onBack = onBack + ) { innerPadding -> + LegalNoticeScreenContent( + innerPadding, + listState + ) } } @@ -95,6 +107,9 @@ private fun LegalNoticeScreenContent( LazyColumn( contentPadding = innerPadding, verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium) + .padding(bottom = PaddingDefaults.Medium), state = listState ) { item { @@ -272,7 +287,7 @@ fun LinkToWeb(linkInfo: String, link: String, icon: ImageVector) { annotatedLink .getStringAnnotations("URL", it, it) .firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) + uriHandler.openUriWhenValid(stringAnnotation.item) } } ) @@ -304,3 +319,13 @@ fun provideLinkForString( end = end ) } + +@LightDarkPreview +@Composable +fun LegalNoticeScreenScaffoldScreenPreview() { + PreviewAppTheme { + LegalNoticeScreenScaffoldContent( + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsOpenSourceLicencesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsOpenSourceLicencesScreen.kt new file mode 100644 index 00000000..dd01275d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsOpenSourceLicencesScreen.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.settings.model.LicenseEntry +import de.gematik.ti.erp.app.settings.model.parseLicenses +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid + +const val LicenseFileUri = "open_source_licenses.json" + +class SettingsOpenSourceLicencesScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + OpenSourceLicensesScreenScaffoldContent( + onBack = navController::popBackStack + ) + } +} + +@Composable +fun OpenSourceLicensesScreenScaffoldContent( + onBack: () -> Unit = {} +) { + val listState = rememberLazyListState() + val licenses = rememberLicenses() + + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + listState = listState, + topBarTitle = stringResource(R.string.settings_legal_licences), + onBack = onBack + ) { + val insetPaddings = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + LazyColumn( + modifier = Modifier.padding(), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues( + start = PaddingDefaults.Medium, + top = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Medium + insetPaddings.calculateBottomPadding() + ) + ) { + @Requirement( + "O.Arch_7", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The list of third party libraries are shown here in a list to the user" + ) + licenses.forEach { + item { + LicenseItem(item = it) + } + } + } + } +} + +@Composable +private fun rememberLicenses(): List { + val context = LocalContext.current + return remember { + val json = context.assets.open(LicenseFileUri).bufferedReader().readText() + parseLicenses(json) + } +} + +@Composable +private fun LicenseItem( + modifier: Modifier = Modifier, + item: LicenseEntry +) { + val title = buildAnnotatedString { + append(item.project) + if (!item.version.isNullOrBlank()) { + append(" (${item.version})") + } + if (!item.year.isNullOrBlank()) { + append(" ${item.year}") + } + } + + val uriHandler = LocalUriHandler.current + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + Text(title, style = AppTheme.typography.h6) + Text(item.dependency, style = AppTheme.typography.body2l, fontStyle = FontStyle.Italic) + item.description?.let { + Text(item.description, style = AppTheme.typography.body2l, fontStyle = FontStyle.Italic) + } + item.developers.takeIf { it.isNotEmpty() }?.let { + Text(item.developers.joinToString(), style = AppTheme.typography.body2) + } + item.url?.let { + ClickableTaggedText( + text = annotatedLinkStringLight(item.url, item.url), + style = AppTheme.typography.body2 + ) { + if (it.tag == "URL") { + uriHandler.openUriWhenValid(it.item) + } + } + } + SpacerSmall() + item.licenses.forEach { + ClickableTaggedText( + text = annotatedLinkStringLight(it.licenseUrl, it.license), + style = AppTheme.typography.body2 + ) { + if (it.tag == "URL") { + uriHandler.openUriWhenValid(it.item) + } + } + } + } +} + +@LightDarkPreview +@Composable +fun OpenSourceLicensesScreenScaffoldScreenPreview() { + PreviewAppTheme { + OpenSourceLicensesScreenScaffoldContent( + onBack = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsProductImprovementsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsProductImprovementsScreen.kt new file mode 100644 index 00000000..2d4a47c1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsProductImprovementsScreen.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import android.content.Context +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Timeline +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens +import de.gematik.ti.erp.app.settings.presentation.rememberProductsImprovementsSettingsController +import de.gematik.ti.erp.app.settings.ui.preview.AnalyticsAllowPreviewData +import de.gematik.ti.erp.app.settings.ui.preview.SettingsProductImprovementPreviewParameter +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LabeledSwitch +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.shortToast + +class SettingsProductImprovementsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val settingsController = rememberProductsImprovementsSettingsController() + val context = LocalContext.current + val listState = rememberLazyListState() + val analyticsState by settingsController.analyticsState + var isAnalyticsAllowed by remember( + analyticsState.analyticsAllowed + ) { mutableStateOf(analyticsState.analyticsAllowed) } + SettingsProductImprovementsScaffold( + context = context, + listState = listState, + isAnalyticsAllowed = isAnalyticsAllowed, + onIsAnalyticsAllowedChange = { isAnalyticsAllowed = !isAnalyticsAllowed }, + onDisallowAnalytics = { + settingsController.changeAnalyticsAllowedState(false) + }, + onNavigateBack = { navController.popBackStack() }, + navigateToAnalytics = { + navController.navigate(SettingsNavigationScreens.SettingsAllowAnalyticsScreen.path()) + } + ) + } +} + +@Composable +fun SettingsProductImprovementsScaffold( + context: Context, + listState: LazyListState, + isAnalyticsAllowed: Boolean, + onIsAnalyticsAllowedChange: () -> Unit, + onDisallowAnalytics: () -> Unit, + onNavigateBack: () -> Unit, + navigateToAnalytics: () -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.settings_product_improvement_headline), + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = onNavigateBack + ) { contentPadding -> + SettingsProductImprovementsScreenContent( + contentPadding = contentPadding, + listState = listState, + isAnalyticsAllowed = isAnalyticsAllowed, + onIsAnalyticsAllowedChange = { + onIsAnalyticsAllowedChange() + }, + onDisallowAnalytics = onDisallowAnalytics, + context = context, + navigateToAnalytics = navigateToAnalytics + ) + } +} + +@Composable +private fun SettingsProductImprovementsScreenContent( + contentPadding: PaddingValues, + listState: LazyListState, + isAnalyticsAllowed: Boolean, + onIsAnalyticsAllowedChange: () -> Unit, + onDisallowAnalytics: () -> Unit, + context: Context, + navigateToAnalytics: () -> Unit +) { + val disallowInfo = stringResource(R.string.settings_tracking_disallow_info) + LazyColumn( + contentPadding = contentPadding, + state = listState + ) { + item { + SpacerMedium() + @Requirement( + "O.Purp_5#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The agreement to the use of the analytics framework could be revoked. " + + "But other agreements cannot be revoked, since the app could not operate properly." + ) + AnalyticsSection( + isAnalyticsAllowed + ) { state -> + onIsAnalyticsAllowedChange() + if (!state) { + onDisallowAnalytics() + context.shortToast(disallowInfo) + } else { + navigateToAnalytics() + } + } + } + } +} + +@Requirement( + "A_19089-01#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "User can opt-in and opt-out of analytics" +) +@Requirement( + "A_19097-01#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Analytics can be enabled and disabled by the user. The user can revoke using the switch." +) +@Requirement( + "O.Purp_5#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Toggle within Settings to enable and disable usage analytics." +) +@Requirement( + "O.Purp_6#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Current Analytics state is inspectable by the user, as most of the user decisions are client side " + + "no history is available. Only the current state can be inspected." +) +@Composable +private fun AnalyticsSection( + analyticsAllowed: Boolean, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit +) { + LabeledSwitch( + checked = analyticsAllowed, + onCheckedChange = onCheckedChange, + modifier = modifier, + icon = Icons.Rounded.Timeline, + header = stringResource(R.string.settings_allow_analytics_header), + description = stringResource(R.string.settings_allow_analytics_info) + ) +} + +@LightDarkPreview +@Composable +fun PreviewSettingsProductImprovementsScreen( + @PreviewParameter(SettingsProductImprovementPreviewParameter::class) + analyticsPreviewData: AnalyticsAllowPreviewData +) { + PreviewAppTheme { + SettingsProductImprovementsScaffold( + context = LocalContext.current, + listState = rememberLazyListState(), + isAnalyticsAllowed = analyticsPreviewData.isAnalyticsAllowed, + onIsAnalyticsAllowedChange = { + analyticsPreviewData.isAnalyticsAllowed = + !analyticsPreviewData.isAnalyticsAllowed + }, + onDisallowAnalytics = {}, + onNavigateBack = {}, + navigateToAnalytics = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsScreen.kt new file mode 100644 index 00000000..a31c52f2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsScreen.kt @@ -0,0 +1,813 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.activity.ComponentActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.outlined.Wysiwyg +import androidx.compose.material.icons.outlined.AllInclusive +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.LockOpen +import androidx.compose.material.icons.outlined.Mail +import androidx.compose.material.icons.outlined.People +import androidx.compose.material.icons.outlined.Poll +import androidx.compose.material.icons.outlined.PrivacyTip +import androidx.compose.material.icons.outlined.Source +import androidx.compose.material.icons.outlined.TireRepair +import androidx.compose.material.icons.outlined.TrackChanges +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.material.icons.rounded.Phone +import androidx.compose.material.icons.rounded.PhoneAndroid +import androidx.compose.material.icons.rounded.VolunteerActivism +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.navigation.TrackingScreenRoutes +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.cardunlock.navigation.CardUnlockRoutes +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.debugsettings.navigation.ShowcaseScreensRoutes +import de.gematik.ti.erp.app.demomode.DemoModeIntent +import de.gematik.ti.erp.app.demomode.DemoModeObserver +import de.gematik.ti.erp.app.demomode.startAppWithDemoMode +import de.gematik.ti.erp.app.demomode.startAppWithNormalMode +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.medicationplan.navigation.MedicationPlanRoutes +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.orderhealthcard.navigation.OrderHealthCardRoutes +import de.gematik.ti.erp.app.profiles.navigation.ProfileRoutes +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.ui.components.Avatar +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import de.gematik.ti.erp.app.settings.model.DebugClickActions +import de.gematik.ti.erp.app.settings.model.HealthCardClickActions +import de.gematik.ti.erp.app.settings.model.LegalClickActions +import de.gematik.ti.erp.app.settings.model.SettingsActions +import de.gematik.ti.erp.app.settings.navigation.SettingsNavigationScreens +import de.gematik.ti.erp.app.settings.presentation.SettingStatesData +import de.gematik.ti.erp.app.settings.presentation.rememberSettingsController +import de.gematik.ti.erp.app.settings.ui.components.AllowScreenshotDialogWithListener +import de.gematik.ti.erp.app.settings.ui.components.GlobalSettingsSection +import de.gematik.ti.erp.app.settings.ui.components.OrganDonationRegisterDialog +import de.gematik.ti.erp.app.settings.ui.preview.LocalIsPreviewMode +import de.gematik.ti.erp.app.settings.ui.preview.SettingsScreenPreviewData +import de.gematik.ti.erp.app.settings.ui.preview.SettingsScreenPreviewProvider +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.buildFeedbackBodyWithDeviceInfo +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.LabelButton +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton +import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.compose.providePhoneIntent +import de.gematik.ti.erp.app.utils.compose.provideWebIntent +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.openMailClient +import org.kodein.di.compose.rememberInstance + +class SettingsScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val buildConfig by rememberInstance() + val organDonationDialogEvent = ComposableEvent() + val context = LocalContext.current + val dialog = LocalDialog.current + val localActivity = LocalActivity.current as? ComponentActivity + val demoModeObserver = localActivity as? DemoModeObserver + val isDemoMode = demoModeObserver?.isDemoMode() ?: false + val settingsController = rememberSettingsController() + + val isMedicationPlanEnabled by settingsController.isMedicationPlanEnabled.collectAsStateWithLifecycle() + + AllowScreenshotDialogWithListener( + dialog = dialog, + event = settingsController.allowScreenshotsEvent, + onAllowScreenshots = { + settingsController.confirmAllowScreenshots() + } + ) + + settingsController.intentEvent.listen { + context.handleIntent(provideWebIntent(it)) + } + + OrganDonationRegisterDialog( + dialog = dialog, + event = organDonationDialogEvent, + createIntent = { settingsController.createOrganDonationRegisterIntent() } + ) + + val settingsActions = SettingsActions( + healthCardClickActions = HealthCardClickActions( + onClickUnlockEgk = { unlockMethod -> + navController.navigate( + CardUnlockRoutes.CardUnlockIntroScreen.path(unlockMethod = unlockMethod.name) + ) + }, + onClickOrderHealthCard = { + navController.navigate(OrderHealthCardRoutes.OrderHealthCardSelectInsuranceCompanyScreen.path()) + } + ), + legalClickActions = LegalClickActions( + onClickLegalNotice = { navController.navigate(SettingsNavigationScreens.SettingsLegalNoticeScreen.path()) }, + onClickDataProtection = { + navController.navigate(SettingsNavigationScreens.SettingsDataProtectionScreen.path()) + }, + onClickOpenSourceLicences = { + navController.navigate(SettingsNavigationScreens.SettingsOpenSourceLicencesScreen.path()) + }, + onClickAdditionalLicences = { + navController.navigate(SettingsNavigationScreens.SettingsAdditionalLicencesScreen.path()) + }, + onClickTermsOfUse = { navController.navigate(SettingsNavigationScreens.SettingsTermsOfUseScreen.path()) } + ), + debugClickActions = DebugClickActions( + onClickDebug = { navController.navigate(MainNavigationScreens.Debug.path()) }, + onClickBottomSheetShowcase = { + navController.navigate(ShowcaseScreensRoutes.BottomSheetShowcaseScreen.path()) + }, + onClickDemoTracking = { navController.navigate(TrackingScreenRoutes.subGraphName()) } + ), + onEnableZoom = settingsController::onEnableZoom, + onDisableZoom = settingsController::onDisableZoom, + onAllowScreenshots = { allow -> + settingsController.onAllowScreenshots(allow) + }, + onClickMedicationPlan = { + navController.navigate(MedicationPlanRoutes.MedicationPlanList.path()) + }, + onClickProductImprovementSettings = { + navController.navigate(SettingsNavigationScreens.SettingsProductImprovementsScreen.path()) + }, + onClickDeviceSecuritySettings = { + navController.navigate(SettingsNavigationScreens.SettingsAppSecurityScreen.path()) + }, + onClickLanguageSettings = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + navController.navigate(SettingsNavigationScreens.SettingsLanguageScreen.path()) + } else { + val intent = Intent(Settings.ACTION_LOCALE_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } + context.startActivity(intent) + } + }, + onClickDemoModeEnd = { + localActivity?.let { DemoModeIntent.startAppWithNormalMode(it) } + }, + onClickDemoMode = { + localActivity?.let { DemoModeIntent.startAppWithDemoMode(it) } + }, + onClickEditProfile = { profileId -> + navController.navigate(ProfileRoutes.ProfileScreen.path(profileId = profileId)) + }, + + onClickOrganDonationRegister = { + organDonationDialogEvent.trigger(Unit) + } + ) + + SettingsScreenScaffold( + listState = rememberLazyListState(), + isDemoMode = isDemoMode, + localActivity = localActivity, + buildConfig = buildConfig, + profilesState = settingsController.profiles.collectAsStateWithLifecycle().value, + context = context, + zoomState = settingsController.zoomState.collectAsStateWithLifecycle(), + screenShotsState = settingsController.screenShotsState.collectAsStateWithLifecycle(), + isMedicationPlanEnabled = isMedicationPlanEnabled, + settingsActions = settingsActions + ) + } +} + +@Composable +private fun SettingsScreenScaffold( + listState: LazyListState, + isDemoMode: Boolean, + localActivity: ComponentActivity?, + buildConfig: BuildConfigInformation, + profilesState: List, + context: Context, + zoomState: State, + screenShotsState: State, + settingsActions: SettingsActions, + isMedicationPlanEnabled: Boolean +) { + val padding = (localActivity as? BaseActivity)?.applicationInnerPadding + + Scaffold( + modifier = Modifier + .testTag(TestTag.Settings.SettingsScreen) + .statusBarsPadding() + ) { contentPadding -> + SettingsScreenContent( + contentPadding = padding?.combineWithInnerScaffold(contentPadding) ?: PaddingValues(), + profilesState = profilesState, + listState = listState, + isDemoMode = isDemoMode, + buildConfig = buildConfig, + context = context, + isMedicationPlanEnabled = isMedicationPlanEnabled, + zoomState = zoomState, + screenShotsState = screenShotsState, + settingsActions = settingsActions + ) + } +} + +@Composable +private fun SettingsScreenContent( + contentPadding: PaddingValues, + profilesState: List, + listState: LazyListState, + isDemoMode: Boolean, + buildConfig: BuildConfigInformation, + context: Context, + zoomState: State, + isMedicationPlanEnabled: Boolean, + screenShotsState: State, + settingsActions: SettingsActions + +) { + LazyColumn( + modifier = Modifier.testTag("settings_screen"), + contentPadding = contentPadding, + state = listState + ) { + @Requirement( + "O.Source_8#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Debug menu is shown only for debug builds." + ) + if (BuildConfigExtension.isInternalDebug) { + item { DebugMenuSection(settingsActions.debugClickActions.onClickDebug) } + } + + item { + ProfileSection(profilesState, settingsActions.onClickEditProfile) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + + if (!isDemoMode) { + item { + HealthCardSection(settingsActions.healthCardClickActions) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + } + + item { + GlobalSettingsSection( + isDemoMode = isDemoMode, + zoomState = zoomState, + screenShotState = screenShotsState, + onEnableZoom = settingsActions.onEnableZoom, + onDisableZoom = settingsActions.onDisableZoom, + isMedicationPlanEnabled = isMedicationPlanEnabled, + onAllowScreenshots = { settingsActions.onAllowScreenshots(true) }, + onDisallowScreenshots = { settingsActions.onAllowScreenshots(false) }, + settingsActions = settingsActions, + onClickMedicationPlan = settingsActions.onClickMedicationPlan + ) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + + item { + ExploreSection( + onClickOrganDonationRegister = { + settingsActions.onClickOrganDonationRegister() + } + ) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + + item { + ContactSection( + darkMode = buildConfig.inDarkTheme(), + language = buildConfig.language(), + versionName = buildConfig.versionName(), + nfcInfo = buildConfig.nfcInformation(context), + phoneModel = buildConfig.model() + ) + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + + item { + LegalSection(settingsActions.legalClickActions) + } + + @Requirement( + "O.Source_8#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Debug menu is shown only for debug builds." + ) + if (BuildConfigExtension.isInternalDebug) { + item { + Divider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Small) + ) + } + item { + Text( + text = "Debug settings", + style = AppTheme.typography.h6, + modifier = Modifier.padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Medium / 2, + top = PaddingDefaults.Medium + ) + ) + } + item { + LabelButton( + Icons.Outlined.TireRepair, + "Debug section", + modifier = Modifier.testTag("debug-section") + ) { + settingsActions.debugClickActions.onClickDebug() + } + } + item { + LabelButton( + Icons.Outlined.AllInclusive, + "Bottom sheet showcase" + ) { + settingsActions.debugClickActions.onClickBottomSheetShowcase() + } + } + item { + Text( + text = "Local Tracking", + style = AppTheme.typography.h6, + modifier = Modifier.padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Medium / 2, + top = PaddingDefaults.Medium + ) + ) + } + item { + LabelButton( + Icons.Outlined.TrackChanges, + "Tracking Debug", + modifier = Modifier.testTag("tracking-debug") + ) { + settingsActions.debugClickActions.onClickDemoTracking() + } + } + } + + item { + AboutSection(modifier = Modifier.padding(top = 76.dp), buildVersionName = buildConfig.versionName()) + } + } +} + +@Composable +private fun ProfileSection( + profiles: List, + onClickEditProfile: (ProfileIdentifier) -> Unit +) { + Column { + Text( + text = stringResource(R.string.settings_profiles_headline), + style = AppTheme.typography.h6, + modifier = Modifier + .padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + top = PaddingDefaults.Medium, + bottom = PaddingDefaults.Small + ) + .testTag("Profiles") + ) + + profiles.forEach { profile -> + ProfileCard( + profile = profile, + onClickEdit = { onClickEditProfile(profile.id) } + ) + } + } + SpacerLarge() +} + +@Composable +private fun ProfileCard( + profile: Profile, + onClickEdit: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(role = Role.Button) { + onClickEdit() + } + .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.ShortMedium) + .testTag(TestTag.Settings.ProfileButton), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + modifier = Modifier.size(SizeDefaults.sixfold), + emptyIcon = Icons.Rounded.PersonOutline, + profile = profile, + iconModifier = Modifier.size(SizeDefaults.doubleHalf) + ) + SpacerMedium() + Text( + modifier = Modifier.weight(1f), + text = profile.name, + style = AppTheme.typography.body1 + ) + Icon(Icons.AutoMirrored.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@Composable +private fun HealthCardSection( + healthCardClickActions: HealthCardClickActions +) { + Column { + Text( + text = stringResource(R.string.health_card_section_header), + style = AppTheme.typography.h6, + modifier = Modifier.padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Small, + top = PaddingDefaults.Medium + ) + ) + + LabelButton( + modifier = Modifier.testTag(TestTag.Settings.OrderNewCardButton), + icon = painterResource(R.drawable.ic_order_egk), + text = stringResource(R.string.health_card_section_order_card) + ) { + healthCardClickActions.onClickOrderHealthCard() + } + + LabelButton( + Icons.AutoMirrored.Outlined.HelpOutline, + stringResource(R.string.health_card_section_unlock_card_forgot_pin) + ) { + healthCardClickActions.onClickUnlockEgk(UnlockMethod.ResetRetryCounterWithNewSecret) + } + + LabelButton( + painterResource(R.drawable.ic_reset_pin), + stringResource(R.string.health_card_section_unlock_card_reset_pin) + ) { + healthCardClickActions.onClickUnlockEgk(UnlockMethod.ChangeReferenceData) + } + + LabelButton( + Icons.Outlined.LockOpen, + stringResource(R.string.health_card_section_unlock_card_no_reset) + ) { + healthCardClickActions.onClickUnlockEgk(UnlockMethod.ResetRetryCounter) + } + } +} + +@Composable +private fun DebugMenuSection(onClickDebug: () -> Unit) { + if (!LocalIsPreviewMode.current) { + OutlinedDebugButton( + text = stringResource(id = R.string.debug_menu), + onClick = { onClickDebug() }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Small, + top = PaddingDefaults.Medium + ) + .testTag(TestTag.Settings.DebugMenuButton) + ) + } +} + +@Composable +private fun LegalSection( + legalClickActions: LegalClickActions +) { + Column { + Text( + text = stringResource(R.string.settings_legal_headline), + style = AppTheme.typography.h6, + modifier = Modifier.padding( + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + bottom = PaddingDefaults.Small, + top = PaddingDefaults.Medium + ) + ) + LabelButton( + Icons.Outlined.Info, + stringResource(R.string.settings_legal_imprint), + modifier = Modifier.testTag("settings/imprint") + ) { + legalClickActions.onClickLegalNotice() + } + @Requirement( + "O.Arch_9#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display data protection within settings" + ) + LabelButton( + Icons.Outlined.PrivacyTip, + stringResource(R.string.settings_legal_dataprotection), + modifier = Modifier.testTag("settings/privacy") + ) { + legalClickActions.onClickDataProtection() + } + LabelButton( + Icons.AutoMirrored.Outlined.Wysiwyg, + stringResource(R.string.settings_legal_tos), + modifier = Modifier.testTag("settings/tos") + ) { + legalClickActions.onClickTermsOfUse() + } + LabelButton( + Icons.Outlined.Code, + stringResource(R.string.settings_legal_licences), + modifier = Modifier.testTag("settings/licences") + ) { + legalClickActions.onClickOpenSourceLicences() + } + LabelButton( + Icons.Outlined.Source, + stringResource(R.string.settings_licence_pharmacy_search), + modifier = Modifier.testTag("settings/additional_licences") + ) { + legalClickActions.onClickAdditionalLicences() + } + } +} + +@Composable +private fun AboutSection( + modifier: Modifier, + buildVersionName: String +) { + Column( + modifier = modifier + .padding(PaddingDefaults.Medium) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CompositionLocalProvider( + LocalTextStyle provides AppTheme.typography.body2, + LocalContentColor provides AppTheme.colors.neutral600 + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.PhoneAndroid, null, modifier = Modifier.size(SizeDefaults.double)) + SpacerTiny() + Text( + stringResource(R.string.about_version, buildVersionName) + ) + } + SpacerTiny() + Text( + stringResource(R.string.about_buildhash, BuildKonfig.GIT_HASH) + ) + } + } +} + +@Composable +fun ExploreSection(onClickOrganDonationRegister: () -> Unit) { + val context = LocalContext.current + SpacerMedium() + Text( + text = stringResource(R.string.settings_explore_headline), + style = AppTheme.typography.h6, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) + SpacerSmall() + LabelButton( + icon = Icons.Rounded.VolunteerActivism, + text = stringResource(R.string.organ_donation_menu_entry), + onClick = { onClickOrganDonationRegister() } + ) + SpacerSmall() + val communityAddress = stringResource(R.string.settings_contact_community_address) + LabelButton( + icon = Icons.Outlined.People, + text = stringResource(R.string.settings_contact_community_label), + onClick = { + context.handleIntent(provideWebIntent(communityAddress)) + } + ) +} + +@Composable +private fun ContactSection( + darkMode: String, + versionName: String, + language: String, + phoneModel: String, + nfcInfo: String +) { + val context = LocalContext.current + val contactHeader = stringResource(R.string.settings_contact_headline) + + Column { + val phoneNumber = stringResource(R.string.settings_contact_hotline_number) + val mailAddress = stringResource(R.string.settings_contact_mail_address) + val subject = stringResource(R.string.settings_feedback_mail_subject) + val surveyAddress = stringResource(R.string.settings_contact_survey_address) + val body = buildFeedbackBodyWithDeviceInfo( + darkMode = darkMode, + versionName = versionName, + language = language, + phoneModel = phoneModel, + nfcInfo = nfcInfo + ) + SpacerMedium() + Text( + text = contactHeader, + style = AppTheme.typography.h6, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) + SpacerSmall() + LabelButton( + icon = Icons.Outlined.Poll, + text = stringResource(R.string.settings_contact_feedback), + onClick = { + context.handleIntent(provideWebIntent(surveyAddress)) + } + ) + LabelButton( + icon = Icons.Outlined.Mail, + text = stringResource(R.string.settings_contact_feedback_form), + onClick = { + openMailClient(context, mailAddress, body, subject) + } + ) + LabelButton( + icon = Icons.Rounded.Phone, + text = stringResource(R.string.settings_contact_hotline), + onClick = { context.handleIntent(providePhoneIntent(phoneNumber)) } + ) + Text( + text = stringResource(R.string.settings_contact_technical_support_description), + style = AppTheme.typography.body2l, + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) + SpacerMedium() + } +} + +@LightDarkPreview +@Composable +fun SettingsScreenPreview( + @PreviewParameter(SettingsScreenPreviewProvider::class) previewData: SettingsScreenPreviewData +) { + val lazyListState = rememberLazyListState() + + CompositionLocalProvider(LocalIsPreviewMode provides true) { + PreviewAppTheme { + val settingsActions = SettingsActions( + healthCardClickActions = HealthCardClickActions( + onClickUnlockEgk = {}, + onClickOrderHealthCard = {} + ), + legalClickActions = LegalClickActions( + onClickLegalNotice = {}, + onClickDataProtection = {}, + onClickOpenSourceLicences = {}, + onClickAdditionalLicences = {}, + onClickTermsOfUse = {} + ), + debugClickActions = DebugClickActions( + onClickDebug = {}, + onClickBottomSheetShowcase = {}, + onClickDemoTracking = {} + ), + onEnableZoom = { previewData.zoomState.value = SettingStatesData.ZoomState(zoomEnabled = true) }, + onDisableZoom = { previewData.zoomState.value = SettingStatesData.ZoomState(zoomEnabled = false) }, + onAllowScreenshots = { previewData.screenShotsState.value = it }, + onClickProductImprovementSettings = {}, + onClickDeviceSecuritySettings = {}, + onClickLanguageSettings = {}, + onClickDemoModeEnd = {}, + onClickDemoMode = {}, + onClickEditProfile = {}, + onClickMedicationPlan = {}, + onClickOrganDonationRegister = { } + ) + + SettingsScreenContent( + contentPadding = PaddingValues(SizeDefaults.zero), + profilesState = previewData.profiles, + listState = lazyListState, + isDemoMode = false, + buildConfig = previewData.buildConfig, + context = LocalContext.current, + zoomState = previewData.zoomState, + screenShotsState = previewData.screenShotsState, + settingsActions = settingsActions, + isMedicationPlanEnabled = false + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsSetAppPasswordScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsSetAppPasswordScreen.kt new file mode 100644 index 00000000..d5b3c2a7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsSetAppPasswordScreen.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.settings.presentation.rememberPasswordSettingsController +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameter +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameterProvider +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.ConfirmationPasswordTextField +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.PasswordStrength +import de.gematik.ti.erp.app.utils.compose.PasswordTextField +import de.gematik.ti.erp.app.utils.compose.presentation.PasswordFieldsData +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class SettingsSetAppPasswordScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val focusRequester = remember { FocusRequester() } + val listState = rememberLazyListState() + + val settingsController = rememberPasswordSettingsController() + val passwordFieldsState by settingsController.passwordFieldsState.collectAsStateWithLifecycle() + + SettingsSetAppPasswordScreenScaffold( + listState = listState, + focusRequester = focusRequester, + passwordFieldsState = passwordFieldsState, + onBack = navController::popBackStack, + setAppPassword = settingsController::setAppPassword, + onPasswordChange = settingsController::onPasswordChange, + onRepeatedPasswordChange = settingsController::onRepeatedPasswordChange + ) + } +} + +@Composable +private fun SettingsSetAppPasswordScreenScaffold( + listState: LazyListState, + focusRequester: FocusRequester, + passwordFieldsState: PasswordFieldsData, + onBack: () -> Unit, + setAppPassword: () -> Unit, + onPasswordChange: (String) -> Unit, + onRepeatedPasswordChange: (String) -> Unit +) { + AnimatedElevationScaffold( + topBarTitle = stringResource(R.string.settings_password_header), + navigationMode = NavigationBarMode.Back, + listState = listState, + onBack = onBack, + bottomBar = { + SettingsSetAppPasswordBottomBar( + onBack = onBack, + setAppPassword = setAppPassword, + passwordFieldsState = passwordFieldsState + ) + } + ) { innerPadding -> + SettingsSetAppPasswordScreenContent( + innerPadding, + listState, + focusRequester, + passwordFieldsState = passwordFieldsState, + onAuthenticateWithPassword = { + setAppPassword() + onBack() + }, + onPasswordChange = { + onPasswordChange(it) + }, + onRepeatedPasswordChange = { + onRepeatedPasswordChange(it) + } + ) + } +} + +@Composable +private fun SettingsSetAppPasswordScreenContent( + innerPadding: PaddingValues, + listState: LazyListState, + focusRequester: FocusRequester, + passwordFieldsState: PasswordFieldsData, + onAuthenticateWithPassword: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onRepeatedPasswordChange: (String) -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium), + contentPadding = innerPadding, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + item { + Text( + text = stringResource(R.string.settings_password_body), + style = AppTheme.typography.body1l + ) + SpacerLarge() + } + item { + PasswordTextField( + modifier = Modifier.fillMaxWidth(), + value = passwordFieldsState.password, + onValueChange = onPasswordChange, + allowAutofill = true, + allowVisiblePassword = true, + label = { + Text(stringResource(R.string.settings_password_entry)) + }, + onSubmit = { focusRequester.requestFocus() } + ) + SpacerTiny() + @Requirement( + "O.Pass_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Usage of password strength evaluation to ensure a secure password for change the Password" + ) + @Requirement( + "O.Pass_2#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Shows password strength within changing the password" + ) + @Requirement( + "O.Pass_3#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Change the Password with a confirmation of the new Password" + ) + PasswordStrength( + modifier = Modifier.fillMaxWidth(), + passwordEvaluation = passwordFieldsState.passwordEvaluation + ) + } + item { + ConfirmationPasswordTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + passwordIsValidAndConsistent = passwordFieldsState.passwordIsValidAndConsistent, + repeatedPasswordHasError = passwordFieldsState.repeatedPasswordHasError, + value = passwordFieldsState.repeatedPassword, + onValueChange = onRepeatedPasswordChange, + onSubmit = { + onAuthenticateWithPassword(passwordFieldsState.password) + } + ) + } + } +} + +@Composable +fun SettingsSetAppPasswordBottomBar( + setAppPassword: () -> Unit, + onBack: () -> Unit, + passwordFieldsState: PasswordFieldsData +) { + val emptyDescription = stringResource(id = R.string.set_app_password_button_state_description_empty) + val notStrongDescription = stringResource(id = R.string.set_app_password_button_state_description_not_strong) + val hasErrorDescription = stringResource(id = R.string.set_app_password_button_state_description_not_the_same) + BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { + Spacer(modifier = Modifier.weight(1f)) + Button( + modifier = Modifier.semantics() { + stateDescription = when { + passwordFieldsState.password.isBlank() -> emptyDescription + !passwordFieldsState.passwordEvaluation.isStrongEnough -> notStrongDescription + passwordFieldsState.repeatedPasswordHasError -> hasErrorDescription + passwordFieldsState.passwordIsValidAndConsistent -> "" + else -> "" // not reachable + } + }, + onClick = { + setAppPassword() + onBack() + }, + enabled = passwordFieldsState.passwordIsValidAndConsistent, + shape = RoundedCornerShape(PaddingDefaults.Small) + ) { + Text(stringResource(R.string.settings_password_save)) + } + SpacerMedium() + } +} + +@LightDarkPreview +@Composable +fun SettingsSetAppPasswordScreenPreview( + @PreviewParameter(SetAppPasswordParameterProvider::class) previewData: SetAppPasswordParameter +) { + PreviewAppTheme { + SettingsSetAppPasswordScreenScaffold( + listState = rememberLazyListState(), + focusRequester = FocusRequester(), + passwordFieldsState = previewData.passwordFieldsState, + onBack = {}, + setAppPassword = {}, + onPasswordChange = {}, + onRepeatedPasswordChange = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsTermsOfUseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsTermsOfUseScreen.kt new file mode 100644 index 00000000..3521dc84 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/screens/SettingsTermsOfUseScreen.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE +import de.gematik.ti.erp.app.webview.WebViewScreen + +@Requirement( + "O.Purp_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Display terms of use as part of the settings. " + + "Webview containing local html without javascript" +) +@Requirement( + "O.Arch_8#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Webview containing local html without javascript" +) +class SettingsTermsOfUseScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + WebViewScreen( + title = stringResource(R.string.onb_terms_of_use), + onBack = navController::popBackStack, + url = URI_TERMS_OF_USE + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOrganDonationRegisterHostsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOrganDonationRegisterHostsUseCase.kt new file mode 100644 index 00000000..ac312116 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOrganDonationRegisterHostsUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.di.EndpointHelper + +class GetOrganDonationRegisterHostsUseCase( + private val endpointHelper: EndpointHelper +) { + val info: String + get() = endpointHelper.getOrganDonationRegisterInfoHost() + + val intent + get() = endpointHelper.getOrganDonationRegisterIntentHost() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetSupportedLanguagesFromXmlUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetSupportedLanguagesFromXmlUseCase.kt new file mode 100644 index 00000000..cf1876ea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/GetSupportedLanguagesFromXmlUseCase.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import io.github.aakira.napier.Napier +import org.xmlpull.v1.XmlPullParser + +class GetSupportedLanguagesFromXmlUseCase( + private val parser: XmlResourceParserWrapper +) { + private var eventType = parser.getEventType() + + @Suppress("NestedBlockDepth") + operator fun invoke(): List { + val languages = mutableListOf() + + try { + parser.next() + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG && parser.name() == "locale") { + val language = parser.getAttributeValue() + language?.let { + languages.add(language) + } + } + eventType = parser.next() + } + } catch (e: Exception) { + Napier.e("Error while parsing locales_config.xml: $e") + return emptyList() + } + return languages + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/XmlResourceParserWrapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/XmlResourceParserWrapper.kt new file mode 100644 index 00000000..868f0ae8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/XmlResourceParserWrapper.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import android.content.res.XmlResourceParser + +interface XmlResourceParserWrapper { + fun getEventType(): Int + fun next(): Int + + fun name(): String + + fun getAttributeValue(): String? +} + +class DefaultXmlResourceParserWrapper( + private val parser: XmlResourceParser +) : XmlResourceParserWrapper { + override fun getEventType(): Int = parser.eventType + override fun next() = parser.next() + override fun name(): String = parser.name + override fun getAttributeValue(): String? = + parser.getAttributeValue( + "http://schemas.android.com/apk/res/android", + "name" + ) +} + +class MockXmlResourceParserWrapper : XmlResourceParserWrapper { + + private var eventType = 2 + private var isNextUsedOnce = false + override fun getEventType(): Int = eventType + + override fun name(): String = "locale" + + override fun next(): Int { + when { + !isNextUsedOnce -> isNextUsedOnce = true + else -> eventType = 1 + } + return eventType + } + + override fun getAttributeValue(): String = "de" +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt deleted file mode 100644 index d45ede86..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.theme - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class AppColors( - val green100: Color, - val green200: Color, - val green300: Color, - val green400: Color, - val green500: Color, - val green600: Color, - val green700: Color, - val green800: Color, - val green900: Color, - - val neutral000: Color, - val neutral025: Color, - val neutral050: Color, - val neutral100: Color, - val neutral200: Color, - val neutral300: Color, - val neutral400: Color, - val neutral500: Color, - val neutral600: Color, - val neutral700: Color, - val neutral800: Color, - val neutral900: Color, - val neutral999: Color, - - val primary100: Color, - val primary200: Color, - val primary300: Color, - val primary400: Color, - val primary500: Color, - val primary600: Color, - val primary700: Color, - val primary800: Color, - val primary900: Color, - - val red100: Color, - val red200: Color, - val red300: Color, - val red400: Color, - val red500: Color, - val red600: Color, - val red700: Color, - val red800: Color, - val red900: Color, - - val yellow100: Color, - val yellow200: Color, - val yellow300: Color, - val yellow400: Color, - val yellow500: Color, - val yellow600: Color, - val yellow700: Color, - val yellow800: Color, - val yellow900: Color, - - val scanOverlayErrorOutline: Color, - val scanOverlayErrorFill: Color, - - val scanOverlaySavedOutline: Color, - val scanOverlaySavedFill: Color, - - val scanOverlayHoldOutline: Color, - val scanOverlayHoldFill: Color -) - -val AppColorsThemeLight = AppColors( - green100 = Color(0xFFEBFFF0), - green200 = Color(0xFFC6F6D5), - green300 = Color(0xFF9AE6B4), - green400 = Color(0xFF68D391), - green500 = Color(0xFF48BB78), - green600 = Color(0xFF38A169), - green700 = Color(0xFF2F855A), - green800 = Color(0xFF276749), - green900 = Color(0xFF22543D), - - neutral000 = Color(0xFFFFFFFF), - neutral025 = Color(0xFFFDFDFD), - neutral050 = Color(0xFFFAFAFA), - neutral100 = Color(0xFFF5F5F5), - neutral200 = Color(0xFFEEEEEE), - neutral300 = Color(0xFFE0E0E0), - neutral400 = Color(0xFFBDBDBD), - neutral500 = Color(0xFF9E9E9E), - neutral600 = Color(0xFF757575), - neutral700 = Color(0xFF616161), - neutral800 = Color(0xFF424242), - neutral900 = Color(0xFF212121), - neutral999 = Color(0xFF000000), - - primary100 = Color(0xFFEBF8FF), - primary200 = Color(0xFFBEE3F8), - primary300 = Color(0xFF90CDF4), - primary400 = Color(0xFF63B3ED), - primary500 = Color(0xFF4299E1), - primary600 = Color(0xFF3182CE), - primary700 = Color(0xFF2B6CB0), - primary800 = Color(0xFF2C5282), - primary900 = Color(0xFF2A4365), - - red100 = Color(0xFFFFEBEB), - red200 = Color(0xFFFED7D7), - red300 = Color(0xFFFEB2B2), - red400 = Color(0xFFFC8181), - red500 = Color(0xFFF56565), - red600 = Color(0xFFE53E3E), - red700 = Color(0xFFC53030), - red800 = Color(0xFF9B2C2C), - red900 = Color(0xFF742A2A), - - yellow100 = Color(0xFFFFFFEB), - yellow200 = Color(0xFFFEFCBF), - yellow300 = Color(0xFFFAF089), - yellow400 = Color(0xFFF6E05E), - yellow500 = Color(0xFFECC94B), - yellow600 = Color(0xFFD69E2E), - yellow700 = Color(0xFFB7791F), - yellow800 = Color(0xFF975A16), - yellow900 = Color(0xFF744210), - - scanOverlayErrorOutline = Color(0xFFF86A6A), - scanOverlayErrorFill = Color(0x33E12D39), - scanOverlaySavedOutline = Color(0xFF00FF64), - scanOverlaySavedFill = Color(0x3300D353), - scanOverlayHoldOutline = Color(0xFFFFFFFF), - scanOverlayHoldFill = Color(0x26FFFFFF) -) - -val AppColorsThemeDark = AppColors( - green100 = Color(0x7F22543D), - green200 = Color(0xFF22543D), - green300 = Color(0xFF276749), - green400 = Color(0xFF2F855A), - green500 = Color(0xFF38A169), - green600 = Color(0xFF48BB78), - green700 = Color(0xFF68D391), - green800 = Color(0xFF9AE6B4), - green900 = Color(0xFFC6F6D5), - - neutral000 = Color(0xFF000000), - neutral025 = Color(0xFF0C0C0C), - neutral050 = Color(0xFF212121), - neutral100 = Color(0xFF424242), - neutral200 = Color(0xFF616161), - neutral300 = Color(0xFF757575), - neutral400 = Color(0xFF9E9E9E), - neutral500 = Color(0xFFBDBDBD), - neutral600 = Color(0xFFE0E0E0), - neutral700 = Color(0xFFEEEEEE), - neutral800 = Color(0xFFF5F5F5), - neutral900 = Color(0xFFFAFAFA), - neutral999 = Color(0xFFFFFFFF), - - primary100 = Color(0x7F33517A), - primary200 = Color(0xFF2A4365), - primary300 = Color(0xFF2C5282), - primary400 = Color(0xFF2B6CB0), - primary500 = Color(0xFF3182CE), - primary600 = Color(0xFF4299E1), - primary700 = Color(0xFF63B3ED), - primary800 = Color(0xFF90CDF4), - primary900 = Color(0xFFBEE3F8), - - red100 = Color(0x7F742A2A), - red200 = Color(0xFF742A2A), - red300 = Color(0xFF9B2C2C), - red400 = Color(0xFFC53030), - red500 = Color(0xFFE53E3E), - red600 = Color(0xFFF56565), - red700 = Color(0xFFFC8181), - red800 = Color(0xFFFEB2B2), - red900 = Color(0xFFFED7D7), - - yellow100 = Color(0x4CECC94B), - yellow200 = Color(0xFF744210), - yellow300 = Color(0xFF975A16), - yellow400 = Color(0xFFB7791F), - yellow500 = Color(0xFFD69E2E), - yellow600 = Color(0xFFECC94B), - yellow700 = Color(0xFFF6E05E), - yellow800 = Color(0xFFFAF089), - yellow900 = Color(0xFFFEFCBF), - - scanOverlayErrorOutline = Color(0xFFF86A6A), - scanOverlayErrorFill = Color(0x33E12D39), - scanOverlaySavedOutline = Color(0xFF00FF64), - scanOverlaySavedFill = Color(0x3300D353), - scanOverlayHoldOutline = Color(0xFFFFFFFF), - scanOverlayHoldFill = Color(0x26FFFFFF) -) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt deleted file mode 100644 index 56e5558e..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.theme - -import androidx.compose.ui.unit.dp - -object PaddingDefaults { - val Tiny = 4.dp - val Small = 8.dp - val ShortMedium = 12.dp - val Medium = 16.dp - val MediumPlus = 18.dp - val Large = 24.dp - val XLarge = 32.dp - val XXLarge = 40.dp - val XXLargeMedium = 56.dp - val Extraordinary = 200.dp -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt deleted file mode 100644 index 8c391ed9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.theme - -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.SizeDefaults.one - -object SizeDefaults { - /** `8dp` in design specification. */ - val one: Dp = 8.dp - - /** `0dp` in design specification. */ - val zero: Dp get() = multiple(0.0) - - /** `1dp` in design specification. */ - val eighth: Dp get() = multiple(.125) - - /** `2dp` in design specification. */ - val quarter: Dp get() = multiple(.25) - - /** `4dp` in design specification. */ - val half: Dp get() = multiple(.5) - - /** `6dp` in design specification. */ - val threeQuarter: Dp get() = multiple(.75) - - /** `10dp` in design specification. */ - val oneQuarter: Dp get() = multiple(1.25) - - /** `12dp` in design specification. */ - val oneHalf: Dp get() = multiple(1.5) - - /** `16dp` in design specification. */ - val double: Dp get() = multiple(2.0) - - /** `20dp` in design specification. */ - val doubleHalf: Dp get() = multiple(2.5) - - /** `24dp` in design specification. */ - val triple: Dp get() = multiple(3.0) - - /** `28dp` in design specification. */ - val tripleHalf: Dp get() = multiple(3.5) - - /** `32dp` in design specification. */ - val fourfold: Dp get() = multiple(4.0) - - /** `40dp` in design specification. */ - val fivefold: Dp get() = multiple(5.0) - - /** `48dp` in design specification. */ - val sixfold: Dp get() = multiple(6.0) - - /** `56dp` in design specification. */ - val sevenfold: Dp get() = multiple(7.0) - - /** `64dp` in design specification. */ - val eightfold: Dp get() = multiple(8.0) - - /** `72dp` in design specification. */ - val ninefold: Dp get() = multiple(9.0) - - /** `80dp` in design specification. */ - val tenfold: Dp get() = multiple(10.0) - - /** `88dp` in design specification. */ - val elevenfold: Dp get() = multiple(11.0) - - /** `96dp` in design specification. */ - val twelvefold: Dp get() = multiple(12.0) - - /** `184dp` in design specification. */ - val twentythreefold: Dp get() = multiple(23.0) - - /** `192dp` in design specification. */ - val twentyfourfold: Dp get() = multiple(24.0) - - /** `304dp` in design specification. */ - val thirtyEightfold: Dp get() = multiple(38.0) - - /** Default horizontal padding. */ - val defaultStartEnd: Dp get() = triple -} - -private fun multiple(multiplier: Float) = one * multiplier - -private fun multiple(multiplier: Double) = multiple(multiplier.toFloat()) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt deleted file mode 100644 index 95455a78..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.features.R - -@Suppress("LongMethod") -@Composable -fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) { - AppColorsThemeDark - } else { - AppColorsThemeLight - } - - val sizes = remember { SizeDefaults } - - val fontFamily = remember { - FontFamily( - Font(R.font.noto_sans_bold, weight = FontWeight.Bold), - Font(R.font.noto_sans_medium, weight = FontWeight.Medium), - Font(R.font.noto_sans_regular, weight = FontWeight.Normal), - Font(R.font.noto_sans_semibold, weight = FontWeight.SemiBold) - ) - } - - val typoColors = AppTypographyColors( - body1l = colors.neutral600, - body2l = colors.neutral600, - subtitle1l = colors.neutral600, - subtitle2l = colors.neutral600, - captionl = colors.neutral600 - ) - - val materialTypo = MaterialTheme.typography.copy( - h1 = MaterialTheme.typography.h1.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W700 - ), - h2 = MaterialTheme.typography.h2.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W700 - ), - h3 = MaterialTheme.typography.h3.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W700 - ), - h4 = MaterialTheme.typography.h4.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W700 - ), - h5 = MaterialTheme.typography.h5.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W700 - ), - h6 = MaterialTheme.typography.h6.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em - ), - subtitle1 = MaterialTheme.typography.subtitle1.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W500 - ), - subtitle2 = MaterialTheme.typography.subtitle2.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W500 - ), - body1 = MaterialTheme.typography.body1.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em - ), - body2 = MaterialTheme.typography.body2.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em - ) - ) - - MaterialTheme( - typography = materialTypo, - colors = Colors( - primary = colors.primary600, - primaryVariant = colors.primary600, - secondary = colors.primary600, - secondaryVariant = colors.primary600, - background = colors.neutral025, - surface = colors.neutral000, - error = colors.red500, - onPrimary = colors.neutral000, - onSecondary = colors.neutral000, - onBackground = colors.neutral900, - onSurface = colors.neutral900, - onError = colors.red900, - isLight = !darkTheme - ), - content = { - val typo = - AppTypography( - body1l = MaterialTheme.typography.body1.copy( - fontFamily = fontFamily, - color = typoColors.body1l, - lineHeight = 1.5.em - ), - body2l = MaterialTheme.typography.body2.copy( - fontFamily = fontFamily, - color = typoColors.body2l, - lineHeight = 1.5.em - ), - subtitle1l = MaterialTheme.typography.subtitle1.copy( - fontFamily = fontFamily, - color = typoColors.subtitle1l, - lineHeight = 1.5.em - ), - subtitle2l = MaterialTheme.typography.subtitle2.copy( - fontFamily = fontFamily, - color = typoColors.subtitle2l, - lineHeight = 1.5.em - ), - caption1l = MaterialTheme.typography.caption.copy( - fontFamily = fontFamily, - color = typoColors.captionl, - lineHeight = 1.5.em - ), - h1 = materialTypo.h1, - h2 = materialTypo.h2, - h3 = materialTypo.h3, - h4 = materialTypo.h4, - h5 = materialTypo.h5, - h6 = materialTypo.h6, - subtitle1 = materialTypo.subtitle1, - subtitle2 = materialTypo.subtitle2, - body1 = materialTypo.body1, - body2 = materialTypo.body2, - button = materialTypo.button, - caption1 = materialTypo.caption, - caption2 = materialTypo.caption.copy(fontWeight = FontWeight.Medium), - overline = materialTypo.overline - ) - - CompositionLocalProvider( - LocalAppColors provides colors, - LocalAppTypographyColors provides typoColors, - LocalAppTypography provides typo, - LocalSizes provides sizes, - content = content - ) - } - ) -} - -object AppTheme { - val colors: AppColors - @Composable - get() = LocalAppColors.current - - val typography: AppTypography - @Composable - get() = LocalAppTypography.current - - val DebugColor = Color(0xFFD71F5F) -} - -private val LocalAppColors = staticCompositionLocalOf { - error("No AppColors provided") -} - -private val LocalAppTypographyColors = staticCompositionLocalOf { - error("No AppTypographyColors provided") -} - -private val LocalAppTypography = staticCompositionLocalOf { - error("No AppTypography provided") -} - -private val LocalSizes = staticCompositionLocalOf { - error("No sized provided") -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt deleted file mode 100644 index a0aebe4d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("LongParameterList") - -package de.gematik.ti.erp.app.theme - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle - -@Immutable -data class AppTypographyColors( - val body1l: Color, - val body2l: Color, - val subtitle1l: Color, - val subtitle2l: Color, - val captionl: Color -) - -@Immutable -class AppTypography( - // overloaded fonts with different lighter color - val body1l: TextStyle, - val body2l: TextStyle, - val subtitle1l: TextStyle, - val subtitle2l: TextStyle, - val caption1l: TextStyle, - // material theme default fonts - val h1: TextStyle, - val h2: TextStyle, - val h3: TextStyle, - val h4: TextStyle, - val h5: TextStyle, - val h6: TextStyle, - val subtitle1: TextStyle, - val subtitle2: TextStyle, - val body1: TextStyle, - val body2: TextStyle, - val button: TextStyle, - val caption1: TextStyle, - val caption2: TextStyle, - val overline: TextStyle -) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt index e33ed978..d640e648 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt @@ -1,26 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("MagicNumber") package de.gematik.ti.erp.app.timeouts.datasource.local import android.content.SharedPreferences import androidx.core.content.edit +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.DEFAULT_INACTIVITY_DURATION import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.DEFAULT_PAUSE_DURATION import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.INACTIVITY_TIMER_ENUM @@ -45,11 +47,18 @@ object TimeoutConstant { const val PAUSE_TIMER_VALUE = "PAUSE_TIMER" const val PAUSE_TIMER_ENUM = "PAUSE_TIMER_ENUM" const val defaultInactivityValue = 10 - const val defaultPauseValue = 30 + const val defaultPauseValue = 60 val defaultInactivityMetric = DurationUnit.MINUTES val defaultPauseMetric = DurationUnit.SECONDS val DEFAULT_INACTIVITY_DURATION = 10.minutes - val DEFAULT_PAUSE_DURATION = 30.seconds + + @Requirement( + "O.Auth_8#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The timer is set to 60 seconds.", + codeLines = 2 + ) + val DEFAULT_PAUSE_DURATION = 60.seconds } class TimeoutsLocalDataSource( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt index 23fff14c..6043fdc7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.di @@ -22,11 +22,12 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource -import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsScreenViewModel import de.gematik.ti.erp.app.timeouts.presentation.DefaultTimeoutsScreenViewModel -import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsScreenViewModel import de.gematik.ti.erp.app.timeouts.repository.DefaultTimeoutRepository +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository import de.gematik.ti.erp.app.timeouts.usecase.GetInactivityMetricUseCase import de.gematik.ti.erp.app.timeouts.usecase.GetPauseMetricUseCase import de.gematik.ti.erp.app.timeouts.usecase.SetInactivityMetricUseCase @@ -53,6 +54,12 @@ val timeoutsSharedPrefsModule = DI.Module("sharedPrefsModule") { val context = instance() val masterKey = instance(ENCRYPTED_PREFS_MASTER_KEY_ALIAS) + @Requirement( + "O.Data_2#3", + "O.Data_3#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Data storage using EncryptedSharedPreferences." + ) EncryptedSharedPreferences.create( context, ENCRYPTED_PREFS_FILE_NAME, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt index 78e1a62f..3771188f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.presentation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt index b1afb1f0..0afb8dab 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.repository diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt index 0e6c5773..58d6b3a9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt index d66ed2e3..20b9c8dd 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt index 31c60b61..1e39de20 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt index b3b0d764..26bbb45c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.usecase diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingGraph.kt index 3a3f2d82..a64c1bbb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingGraph.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingGraph.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.troubleshooting.navigation @@ -22,10 +22,10 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation import de.gematik.ti.erp.app.navigation.renderComposable -import de.gematik.ti.erp.app.troubleshooting.ui.TroubleShootingDeviceOnTopScreen -import de.gematik.ti.erp.app.troubleshooting.ui.TroubleShootingFindNfcPositionScreen -import de.gematik.ti.erp.app.troubleshooting.ui.TroubleShootingIntroScreen -import de.gematik.ti.erp.app.troubleshooting.ui.TroubleShootingNoSuccessScreen +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingDeviceOnTopScreen +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingFindNfcPositionScreen +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingIntroScreen +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingNoSuccessScreen fun NavGraphBuilder.troubleShootingGraph( startDestination: String = TroubleShootingRoutes.TroubleShootingIntroScreen.route, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingRoutes.kt index 9a756870..1f9e2928 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingRoutes.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/navigation/TroubleShootingRoutes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.troubleshooting.navigation diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreen.kt deleted file mode 100644 index 5c925e6c..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.troubleshooting.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextTipButton -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class TroubleShootingDeviceOnTopScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - TroubleShootingScaffold( - title = stringResource(R.string.cdw_troubleshooting_page_b_title), - onBack = { navController.popBackStack() }, - bottomBarButton = { - TroubleShootingNextTipButton( - onClick = { - navController.navigate(TroubleShootingRoutes.TroubleShootingFindNfcPositionScreen.path()) - } - ) - } - ) { - TroubleShootingDeviceOnTopScreenContent( - onClickTryMe = { - navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) - } - ) - } - } -} - -@Composable -private fun TroubleShootingDeviceOnTopScreenContent( - onClickTryMe: () -> Unit -) { - Column { - TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_b_tip1)) - SpacerMedium() - TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_b_tip2)) - SpacerLarge() - TroubleShootingTryMeButton { - onClickTryMe() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreen.kt deleted file mode 100644 index 2c53ceb9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreen.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.troubleshooting.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.platform.UriHandler -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextButton -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.annotatedLinkString -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource - -class TroubleShootingFindNfcPositionScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val uriHandler = LocalUriHandler.current - TroubleShootingScaffold( - title = stringResource(R.string.cdw_troubleshooting_page_c_title), - onBack = { navController.popBackStack() }, - bottomBarButton = { - TroubleShootingNextButton( - onClick = { - navController.navigate(TroubleShootingRoutes.TroubleShootingNoSuccessScreen.path()) - } - ) - } - ) { - TroubleShootingFindNfcPositionScreenContent( - uriHandler = uriHandler, - onClickTryMe = { - navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) - } - ) - } - } -} - -@Composable -private fun TroubleShootingFindNfcPositionScreenContent( - uriHandler: UriHandler, - onClickTryMe: () -> Unit -) { - Column { - val firstTip = annotatedStringResource( - R.string.cdw_troubleshooting_page_c_tip1, - annotatedLinkString( - stringResource(R.string.cdw_troubleshooting_page_c_tip1_samsung_url), - stringResource(R.string.cdw_troubleshooting_page_c_tip1_samsung) - ) - ) - val secondTip = annotatedStringResource( - R.string.cdw_troubleshooting_page_c_tip2, - annotatedLinkString( - stringResource(R.string.cdw_troubleshooting_page_c_tip2_google_url), - stringResource(R.string.cdw_troubleshooting_page_c_tip2_google) - ) - ) - TroubleShootingTip(firstTip, onClickText = { tag, item -> - when (tag) { - "URL" -> uriHandler.openUri(item) - } - }) - SpacerMedium() - TroubleShootingTip(secondTip, onClickText = { tag, item -> - when (tag) { - "URL" -> uriHandler.openUri(item) - } - }) - SpacerLarge() - TroubleShootingTryMeButton { - onClickTryMe() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreen.kt deleted file mode 100644 index 1a67cef1..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.troubleshooting.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextTipButton -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium - -class TroubleShootingIntroScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - TroubleShootingScaffold( - title = stringResource(R.string.cdw_troubleshooting_page_a_title), - onBack = { navController.popBackStack() }, - bottomBarButton = { - TroubleShootingNextTipButton( - onClick = { navController.navigate(TroubleShootingRoutes.TroubleShootingDeviceOnTopScreen.path()) } - ) - } - ) { - TroubleShootingIntroScreenContent( - onClickTryMe = { - navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) - } - ) - } - } -} - -@Composable -private fun TroubleShootingIntroScreenContent( - onClickTryMe: () -> Unit -) { - Column { - TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip1)) - SpacerMedium() - TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip2)) - SpacerMedium() - TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip3)) - SpacerLarge() - TroubleShootingTryMeButton { - onClickTryMe() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreen.kt deleted file mode 100644 index cd19f512..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreen.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.troubleshooting.ui - -import android.content.Context -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.info.BuildConfigInformation -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingCloseButton -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingContactUsButton -import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold -import de.gematik.ti.erp.app.utils.buildFeedbackBodyWithDeviceInfo -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.openMailClient -import org.kodein.di.compose.rememberInstance - -class TroubleShootingNoSuccessScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - @Composable - override fun Content() { - val buildConfig by rememberInstance() - val context = LocalContext.current - TroubleShootingScaffold( - title = stringResource(R.string.cdw_troubleshooting_no_success_title), - onBack = { navController.popBackStack() }, - bottomBarButton = { - TroubleShootingCloseButton( - onClick = { - navController.popBackStack( - TroubleShootingRoutes.TroubleShootingIntroScreen.route, - inclusive = true - ) - } - ) - } - ) { - TroubleShootingNoSuccessScreenContent( - context = context, - buildConfig = buildConfig - ) - } - } -} - -@Composable -private fun TroubleShootingNoSuccessScreenContent( - context: Context, - buildConfig: BuildConfigInformation -) { - val mailAddress = stringResource(R.string.settings_contact_mail_address) - val subject = stringResource(R.string.settings_feedback_mail_subject) - val body = buildFeedbackBodyWithDeviceInfo( - darkMode = buildConfig.inDarkTheme(), - language = buildConfig.language(), - versionName = buildConfig.versionName(), - nfcInfo = buildConfig.nfcInformation(context), - phoneModel = buildConfig.model() - ) - - Column { - Text( - text = stringResource(R.string.cdw_troubleshooting_no_success_body), - style = AppTheme.typography.body1 - ) - SpacerLarge() - TroubleShootingContactUsButton( - Modifier.align(Alignment.CenterHorizontally), - onClick = { - openMailClient(context, mailAddress, body, subject) - } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingButtons.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingButtons.kt index 2a689c04..07b79bcf 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingButtons.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingButtons.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.troubleshooting.ui.components @@ -38,11 +38,11 @@ import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText import de.gematik.ti.erp.app.utils.compose.PrimaryButton import de.gematik.ti.erp.app.utils.compose.SecondaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall @Composable fun RowScope.TroubleShootingNextTipButton( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingInfo.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingInfo.kt index 1a743371..b3dd88ce 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingInfo.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingInfo.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.troubleshooting.ui.components @@ -36,8 +36,8 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny @Composable fun TroubleShootingInfo( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingScaffold.kt index a3e00b3c..5e83b14b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/components/TroubleShootingScaffold.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.troubleshooting.ui.components @@ -37,9 +37,9 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerLarge import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerLarge @Composable fun TroubleShootingScaffold( @@ -78,7 +78,7 @@ fun TroubleShootingScaffold( Text( title, style = AppTheme.typography.h6, - textAlign = TextAlign.Center, + textAlign = TextAlign.Left, modifier = Modifier.fillMaxWidth() ) SpacerLarge() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/preview/TroubleShootingScreenPreviewProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/preview/TroubleShootingScreenPreviewProvider.kt new file mode 100644 index 00000000..63b69a99 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/preview/TroubleShootingScreenPreviewProvider.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui.preview + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.info.BuildConfigInformation + +data class TroubleShootingScreenPreviewData( + val name: String, + val buildConfig: BuildConfigInformation +) + +object MockBuildConfigInformation : BuildConfigInformation { + override fun versionName(): String = "1.25.0-RC2-debug" + override fun versionCode(): String = "3598" + override fun model(): String = "samsung SM-S921B (e1sxeea)" + override fun language(): String = "en" + + @Composable + override fun inDarkTheme(): String = "on" + override fun nfcInformation(context: Context): String = "available" + override fun isMockedApp(): Boolean = false +} + +class TroubleShootingScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + TroubleShootingScreenPreviewData( + name = "TroubleShootingNoSuccessScreen", + buildConfig = MockBuildConfigInformation + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingDeviceOnTopScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingDeviceOnTopScreen.kt new file mode 100644 index 00000000..a20f7a8b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingDeviceOnTopScreen.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextTipButton +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class TroubleShootingDeviceOnTopScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_b_title), + onBack = { navController.popBackStack() }, + bottomBarButton = { + TroubleShootingNextTipButton( + onClick = { + navController.navigate(TroubleShootingRoutes.TroubleShootingFindNfcPositionScreen.path()) + } + ) + } + ) { + TroubleShootingDeviceOnTopScreenContent( + onClickTryMe = { + navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) + } + ) + } + } +} + +@Composable +private fun TroubleShootingDeviceOnTopScreenContent( + onClickTryMe: () -> Unit +) { + Column { + TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_b_tip1)) + SpacerMedium() + TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_b_tip2)) + SpacerLarge() + TroubleShootingTryMeButton { + onClickTryMe() + } + } +} + +@LightDarkPreview +@Composable +fun TroubleShootingScaffoldPreview() { + PreviewAppTheme { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_b_title), + onBack = { }, + bottomBarButton = { + TroubleShootingNextTipButton( + onClick = { } + ) + } + ) { + TroubleShootingDeviceOnTopScreenContent( + onClickTryMe = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingFindNfcPositionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingFindNfcPositionScreen.kt new file mode 100644 index 00000000..0a1cfda2 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingFindNfcPositionScreen.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextButton +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.annotatedLinkString +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid + +class TroubleShootingFindNfcPositionScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val uriHandler = LocalUriHandler.current + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_c_title), + onBack = { navController.popBackStack() }, + bottomBarButton = { + TroubleShootingNextButton( + onClick = { + navController.navigate(TroubleShootingRoutes.TroubleShootingNoSuccessScreen.path()) + } + ) + } + ) { + TroubleShootingFindNfcPositionScreenContent( + uriHandler = uriHandler, + onClickTryMe = { + navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) + } + ) + } + } +} + +@Composable +private fun TroubleShootingFindNfcPositionScreenContent( + uriHandler: UriHandler, + onClickTryMe: () -> Unit +) { + Column { + val firstTip = annotatedStringResource( + R.string.cdw_troubleshooting_page_c_tip1, + annotatedLinkString( + stringResource(R.string.cdw_troubleshooting_page_c_tip1_samsung_url), + stringResource(R.string.cdw_troubleshooting_page_c_tip1_samsung) + ) + ) + val secondTip = annotatedStringResource( + R.string.cdw_troubleshooting_page_c_tip2, + annotatedLinkString( + stringResource(R.string.cdw_troubleshooting_page_c_tip2_google_url), + stringResource(R.string.cdw_troubleshooting_page_c_tip2_google) + ) + ) + TroubleShootingTip(firstTip, onClickText = { tag, item -> + when (tag) { + "URL" -> uriHandler.openUriWhenValid(item) + } + }) + SpacerMedium() + TroubleShootingTip(secondTip, onClickText = { tag, item -> + when (tag) { + "URL" -> uriHandler.openUriWhenValid(item) + } + }) + SpacerLarge() + TroubleShootingTryMeButton { + onClickTryMe() + } + } +} + +@LightDarkPreview +@Composable +fun TroubleShootingFindNfcPositionScaffoldPreview() { + val uriHandler = LocalUriHandler.current + PreviewAppTheme { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_c_title), + onBack = {}, + bottomBarButton = { + TroubleShootingNextButton( + onClick = {} + ) + } + ) { + TroubleShootingFindNfcPositionScreenContent( + uriHandler = uriHandler, + onClickTryMe = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingIntroScreen.kt new file mode 100644 index 00000000..2ebb78de --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingIntroScreen.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingNextTipButton +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTip +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingTryMeButton +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +class TroubleShootingIntroScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_a_title), + onBack = { navController.popBackStack() }, + bottomBarButton = { + TroubleShootingNextTipButton( + onClick = { navController.navigate(TroubleShootingRoutes.TroubleShootingDeviceOnTopScreen.path()) } + ) + } + ) { + TroubleShootingIntroScreenContent( + onClickTryMe = { + navController.popBackStack(TroubleShootingRoutes.TroubleShootingIntroScreen.route, inclusive = true) + } + ) + } + } +} + +@Composable +private fun TroubleShootingIntroScreenContent( + onClickTryMe: () -> Unit +) { + Column { + TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip1)) + SpacerMedium() + TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip2)) + SpacerMedium() + TroubleShootingTip(stringResource(R.string.cdw_troubleshooting_page_a_tip3)) + SpacerLarge() + TroubleShootingTryMeButton { + onClickTryMe() + } + } +} + +@LightDarkPreview +@Composable +fun TroubleShootingIntroScreenPreview() { + PreviewAppTheme { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_page_a_title), + onBack = {}, + bottomBarButton = { + TroubleShootingNextTipButton( + onClick = {} + ) + } + ) { + TroubleShootingIntroScreenContent( + onClickTryMe = {} + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingNoSuccessScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingNoSuccessScreen.kt new file mode 100644 index 00000000..366d970a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/screens/TroubleShootingNoSuccessScreen.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui.screens + +import android.content.Context +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.troubleshooting.navigation.TroubleShootingRoutes +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingCloseButton +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingContactUsButton +import de.gematik.ti.erp.app.troubleshooting.ui.components.TroubleShootingScaffold +import de.gematik.ti.erp.app.troubleshooting.ui.preview.TroubleShootingScreenPreviewData +import de.gematik.ti.erp.app.troubleshooting.ui.preview.TroubleShootingScreenPreviewProvider +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.buildFeedbackBodyWithDeviceInfo +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.openMailClient +import org.kodein.di.compose.rememberInstance + +class TroubleShootingNoSuccessScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + @Composable + override fun Content() { + val buildConfig by rememberInstance() + val context = LocalContext.current + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_no_success_title), + onBack = { navController.popBackStack() }, + bottomBarButton = { + TroubleShootingCloseButton( + onClick = { + navController.popBackStack( + TroubleShootingRoutes.TroubleShootingIntroScreen.route, + inclusive = true + ) + } + ) + } + ) { + TroubleShootingNoSuccessScreenContent( + context = context, + buildConfig = buildConfig + ) + } + } +} + +@Composable +private fun TroubleShootingNoSuccessScreenContent( + context: Context, + buildConfig: BuildConfigInformation +) { + val mailAddress = stringResource(R.string.settings_contact_mail_address) + val subject = stringResource(R.string.settings_feedback_mail_subject) + val body = buildFeedbackBodyWithDeviceInfo( + darkMode = buildConfig.inDarkTheme(), + language = buildConfig.language(), + versionName = buildConfig.versionName(), + nfcInfo = buildConfig.nfcInformation(context), + phoneModel = buildConfig.model() + ) + + Column { + Text( + text = stringResource(R.string.cdw_troubleshooting_no_success_body), + style = AppTheme.typography.body1 + ) + SpacerLarge() + TroubleShootingContactUsButton( + Modifier.align(Alignment.CenterHorizontally), + onClick = { + openMailClient(context, mailAddress, body, subject) + } + ) + } +} + +@LightDarkPreview +@Composable +fun TroubleShootingNoSuccessScreenPreview( + @PreviewParameter(TroubleShootingScreenPreviewProvider::class) previewData: TroubleShootingScreenPreviewData +) { + PreviewAppTheme { + TroubleShootingScaffold( + title = stringResource(R.string.cdw_troubleshooting_no_success_title), + onBack = { }, + bottomBarButton = { + TroubleShootingCloseButton( + onClick = { } + ) + } + ) { + TroubleShootingNoSuccessScreenContent( + context = LocalContext.current, + buildConfig = previewData.buildConfig + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationGraph.kt new file mode 100644 index 00000000..d3ea43f5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationGraph.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.userauthentication.ui.screens.UserAuthenticationScreen + +fun NavGraphBuilder.userAuthenticationGraph( + startDestination: String = UserAuthenticationRoutes.UserAuthenticationScreen.route, + navController: NavController +) { + navigation( + startDestination = startDestination, + route = UserAuthenticationRoutes.subGraphName() + ) { + renderComposable( + route = UserAuthenticationRoutes.UserAuthenticationScreen.route, + arguments = UserAuthenticationRoutes.UserAuthenticationScreen.arguments + ) { navEntry -> + UserAuthenticationScreen( + navController = navController, + navBackStackEntry = navEntry + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationRoutes.kt new file mode 100644 index 00000000..bf3f430f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/navigation/UserAuthenticationRoutes.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.navigation + +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes + +object UserAuthenticationRoutes : NavigationRoutes { + override fun subGraphName() = "userAuthentication" + + object UserAuthenticationScreen : Routes(NavigationRouteNames.UserAuthenticationScreen.name) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt index 8469a6cf..fd6d0d2b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.userauthentication.observer @@ -22,23 +22,17 @@ import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat -import de.gematik.ti.erp.app.Requirement import io.github.aakira.napier.Napier -@Requirement( - "A_21584", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Only biometric means provided by the operating system are used." -) open class BiometricPromptBuilder(val activity: AppCompatActivity) { private val executor = ContextCompat.getMainExecutor(activity) private val biometricManager = BiometricManager.from(activity) - private val authenticators = fetchAuthenticators(biometricManager) + private val authenticators = bestSecureOption(biometricManager) private fun authenticationCallback( onSuccess: () -> Unit, onFailure: (() -> Unit)? = null, - onError: (() -> Unit)? = null + onError: ((String, Int) -> Unit)? = null ): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationFailed() { super.onAuthenticationFailed() @@ -49,13 +43,13 @@ open class BiometricPromptBuilder(val activity: AppCompatActivity) { onFailure?.invoke() } - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) + override fun onAuthenticationError(errorCode: Int, systemMessage: CharSequence) { + super.onAuthenticationError(errorCode, systemMessage) Napier.i( tag = "BiometricPrompt", - message = "authentication error $errString" + message = "authentication error $systemMessage" ) - onError?.invoke() + onError?.invoke(systemMessage.toString(), errorCode) } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -68,7 +62,7 @@ open class BiometricPromptBuilder(val activity: AppCompatActivity) { } } - fun buildPromptInfo( + fun buildPromptInfoWithBestSecureOption( title: String, description: String = "", negativeButton: String @@ -80,40 +74,44 @@ open class BiometricPromptBuilder(val activity: AppCompatActivity) { setNegativeButtonText(negativeButton) } }.setAllowedAuthenticators(authenticators) + .setConfirmationRequired(false) + .build() + + fun buildPromptInfoWithAllAuthenticatorsAvailable( + title: String, + description: String = "" + ): BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setDescription(description) + .setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_STRONG + or BiometricManager.Authenticators.BIOMETRIC_WEAK + or BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + .setConfirmationRequired(false) .build() fun buildBiometricPrompt( onSuccess: () -> Unit, onFailure: (() -> Unit)? = null, - onError: (() -> Unit)? = null + onError: ((String, Int) -> Unit)? = null ): BiometricPrompt = BiometricPrompt(activity, executor, authenticationCallback(onSuccess, onFailure, onError)) - @Requirement( - "A_21582", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Selection of the best available authentication option on the device." - ) @Suppress("ReturnCount") - private fun fetchAuthenticators(biometricManager: BiometricManager): Int { + private fun bestSecureOption(biometricManager: BiometricManager): Int { if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == - BiometricManager.BIOMETRIC_SUCCESS || - biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + BiometricManager.BIOMETRIC_SUCCESS ) { return BiometricManager.Authenticators.BIOMETRIC_STRONG } if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == - BiometricManager.BIOMETRIC_SUCCESS || - biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + BiometricManager.BIOMETRIC_SUCCESS ) { return BiometricManager.Authenticators.BIOMETRIC_WEAK } if (biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == - BiometricManager.BIOMETRIC_SUCCESS || - biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + BiometricManager.BIOMETRIC_SUCCESS ) { return BiometricManager.Authenticators.DEVICE_CREDENTIAL } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt index c2b4792a..f70f6f9e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.userauthentication.observer @@ -26,7 +26,6 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.model.SettingsData.AuthenticationMode.Password import de.gematik.ti.erp.app.settings.repository.SettingsRepository import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository import io.github.aakira.napier.Napier @@ -54,7 +53,7 @@ import java.time.Duration sealed class AuthenticationModeAndMethod { data object None : AuthenticationModeAndMethod() data object Authenticated : AuthenticationModeAndMethod() - data class AuthenticationRequired(val method: SettingsData.AuthenticationMode, val nrOfFailedAuthentications: Int) : + data class AuthenticationRequired(val authentication: SettingsData.Authentication) : AuthenticationModeAndMethod() } @@ -62,11 +61,12 @@ private const val RESET_TIMEOUT = -1L // tag::AuthenticationUseCase[] @Requirement( - "O.Auth_8", + "O.Auth_9#1", + "O.Plat_9#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "A Timer is used to measure the time a user is inactive. Every user interaction resets the timer." + rationale = "A Timer is observing the app lifecycle to notice a change in app state." ) -class InactivityTimeoutObserver( +class InactivityTimeoutObserver( // TODO: Move to different package private val settingsRepository: SettingsRepository, private val timeoutRepo: TimeoutRepository, dispatcher: CoroutineDispatcher = Dispatchers.Default @@ -89,31 +89,26 @@ class InactivityTimeoutObserver( combineTransform( lifecycle, authRequired, - settingsRepository.authenticationMode, - settingsRepository.general - ) { lifecycle, authRequired, authenticationMode, settings -> - unspecifiedAuthentication = authenticationMode is SettingsData.AuthenticationMode.Unspecified + settingsRepository.authentication + ) { lifecycle, authRequired, authentication -> + unspecifiedAuthentication = authentication.methodIsUnspecified when (lifecycle) { Lifecycle.Created -> { - this@InactivityTimeoutObserver.authRequired.value = when (authenticationMode) { - SettingsData.AuthenticationMode.Unspecified -> false - else -> true - } + this@InactivityTimeoutObserver.authRequired.value = !authentication.methodIsUnspecified this@InactivityTimeoutObserver.lifecycle.value = Lifecycle.Running } Lifecycle.Started -> { this@InactivityTimeoutObserver.lifecycle.value = Lifecycle.Running } Lifecycle.Running -> { - when (authenticationMode) { - SettingsData.AuthenticationMode.Unspecified -> - emit(AuthenticationModeAndMethod.Authenticated) - else -> if (authRequired) { + if (authentication.methodIsUnspecified) { + emit(AuthenticationModeAndMethod.Authenticated) + } else { + if (authRequired) { emit( AuthenticationModeAndMethod.AuthenticationRequired( - authenticationMode, - settings.authenticationFails + authentication = authentication ) ) } else { @@ -139,6 +134,12 @@ class InactivityTimeoutObserver( Napier.i { "Restarted inactivity timer for ${Duration.ofMillis(timeout)}" } delay(timeout) requireAuthentication() + @Requirement( + "O.Auth_9#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The timer is reset on user interaction", + codeLines = 2 + ) currentTimeout = RESET_TIMEOUT } else { inactivityTimerChannel.tryEmit(timeoutRepo.getInactivityTimeout().inWholeMilliseconds) @@ -158,9 +159,15 @@ class InactivityTimeoutObserver( .shareIn(scope = scope, started = SharingStarted.Lazily, replay = 1) // end::AuthenticationUseCase[] + @Requirement( + "O.Auth_7#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Password check is done in a secure way.", + codeLines = 10 + ) suspend fun isPasswordValid(password: String): Boolean = - settingsRepository.authenticationMode.map { - (it as? Password)?.isValid(password) ?: false + settingsRepository.authentication.map { + it.password?.isValid(password) ?: false }.first() fun resetInactivityTimer() { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt index c5a71b98..d799f27d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt @@ -1,32 +1,38 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.userauthentication.observer import androidx.lifecycle.ProcessLifecycleOwner +import de.gematik.ti.erp.app.Requirement /** * [ProcessLifecycleObserver] is a singleton class that oversees the processes that are observing the app lifecycle */ -class ProcessLifecycleObserver( +class ProcessLifecycleObserver( // TODO: Move to different package private val processLifecycleOwner: ProcessLifecycleOwner.Companion, private val inactivityTimeoutObserver: InactivityTimeoutObserver ) { + @Requirement( + "O.Auth_8#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The app-lifecycle is continuously observed to notice a change in app state." + ) fun observeForInactivity() { processLifecycleOwner.get().lifecycle.apply { addObserver(inactivityTimeoutObserver) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/presentation/UserAuthenticationController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/presentation/UserAuthenticationController.kt new file mode 100644 index 00000000..d2212620 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/presentation/UserAuthenticationController.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.presentation + +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData.AuthenticationState +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.uistate.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class AuthenticationController( + private val inactivityTimeoutObserver: InactivityTimeoutObserver, + private val biometricPromptBuilder: BiometricPromptBuilder, + private val promptInfo: BiometricPrompt.PromptInfo +) : Controller() { + val authenticationWithPasswordEvent = ComposableEvent() + + private val _authentication = MutableStateFlow(AuthenticationStateData.defaultAuthenticationState) + + init { + controllerScope.launch { + inactivityTimeoutObserver.authenticationModeAndMethod + .map { + when (it) { + is AuthenticationModeAndMethod.AuthenticationRequired -> AuthenticationState( + it.authentication + ) + else -> AuthenticationState( // only reached in failure state + SettingsData.Authentication( + password = null, + deviceSecurity = false, + failedAuthenticationAttempts = 0 + ) + ) + } + } + .collect { authenticationState -> + _authentication.update { + it.copy( + authentication = authenticationState.authentication + ) + } + } + } + } + + val authenticationState: StateFlow = _authentication + + val uiState = _authentication.map { + when { + it.authentication.failedAuthenticationAttempts == 0 -> UiState.Empty() + it.biometricError != null -> UiState.Error(it) + else -> { + UiState.Data(it) + } + } + } + + private val _password: MutableStateFlow = MutableStateFlow("") + + fun onChangePassword(password: String) { + _password.value = password + } + + fun onClickAuthenticate( + onSuccessLeaveAuthScreen: () -> Unit + ) { + when { + authenticationState.value.authentication.methodIsPassword -> { + authenticationWithPasswordEvent.trigger(Unit) + } + else -> { + onAuthenticateWithDeviceSecurity(onSuccessLeaveAuthScreen) + } + } + } + + fun onAuthenticateWithPassword( + onSuccessLeaveAuthScreen: () -> Unit + ) { + controllerScope.launch { + @Requirement( + "O.Pass_4#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "If the password is not correct, the function onFailedAuthentication is called." + ) + if (inactivityTimeoutObserver.isPasswordValid(_password.value)) { + onSuccessfulAuthentication( + onSuccessLeaveAuthScreen = onSuccessLeaveAuthScreen + ) + } else { + onFailedAuthentication() + } + } + } + + private fun onAuthenticateWithDeviceSecurity( + onSuccessLeaveAuthScreen: () -> Unit + ) { + val prompt = biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + onSuccessfulAuthentication( + onSuccessLeaveAuthScreen = onSuccessLeaveAuthScreen + ) + }, + onFailure = { + onFailedAuthentication() + }, + onError = { errorMessage, errorCode -> + _authentication.update { + it.copy( + biometricError = AuthenticationStateData.BiometricError( + name = errorMessage, + code = errorCode + ) + ) + } + } + ) + prompt.authenticate(promptInfo) + } + + fun onSuccessfulAuthentication( + onSuccessLeaveAuthScreen: () -> Unit + ) { + controllerScope.launch { + inactivityTimeoutObserver.resetNumberOfAuthenticationFailures() + inactivityTimeoutObserver.authenticated() + } + onSuccessLeaveAuthScreen() + } + + @Requirement( + "O.Pass_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Increments the number of authentication failures when the user fails to authenticate." + ) + private fun onFailedAuthentication() { + controllerScope.launch { + inactivityTimeoutObserver.incrementNumberOfAuthenticationFailures() + } + } +} + +@Composable +fun rememberAuthenticationController(): AuthenticationController { + val inactivityTimeoutObserver by rememberInstance() + val activity = LocalActivity.current + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } + val promptInfo = biometricPromptBuilder.buildPromptInfoWithAllAuthenticatorsAvailable( + title = stringResource(R.string.auth_prompt_headline), + description = stringResource(R.string.alternate_auth_info) + ) + + return remember { + AuthenticationController( + inactivityTimeoutObserver = inactivityTimeoutObserver, + biometricPromptBuilder = biometricPromptBuilder, + promptInfo = promptInfo + ) + } +} + +object AuthenticationStateData { + @Immutable + data class AuthenticationState( + val authentication: SettingsData.Authentication, + val biometricError: BiometricError? = null + ) : Throwable() + + val defaultAuthenticationState = AuthenticationState( + authentication = SettingsData.Authentication( + password = null, + deviceSecurity = false, + failedAuthenticationAttempts = 0 + ), + biometricError = null + ) + + data class BiometricError( + val name: String? = null, + val code: Int? = null + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationHintCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationHintCard.kt deleted file mode 100644 index 9733f10a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationHintCard.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.userauthentication.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.HintCard -import de.gematik.ti.erp.app.utils.compose.HintCardDefaults -import de.gematik.ti.erp.app.utils.compose.HintSmallImage -import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource - -@Composable -fun AuthenticationHintCard( - state: AuthenticationStateData.AuthenticationState -) { - Box( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) { - HintCard( - modifier = Modifier.padding(vertical = PaddingDefaults.Medium), - properties = HintCardDefaults.flatProperties( - backgroundColor = AppTheme.colors.red100 - ), - image = { - HintSmallImage( - painterResource(R.drawable.oh_no_girl_hint_red), - innerPadding = it - ) - }, - title = { Text(stringResource(R.string.auth_error_failed_auths_headline)) }, - body = { - Text( - annotatedPluralsResource( - R.plurals.auth_error_failed_auths_info, - state.nrOfAuthFailures, - AnnotatedString(state.nrOfAuthFailures.toString()) - ) - ) - } - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt deleted file mode 100644 index a695687a..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ -@file:Suppress("ReturnCount") - -package de.gematik.ti.erp.app.userauthentication.ui - -import androidx.appcompat.app.AppCompatActivity -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.utils.compose.shortToast - -// tag::BiometricPromptAndBestSecureOption[] - -@Requirement( - "A_21584", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Only biometric means provided by the operating system are used." -) -@Composable -fun BiometricPromptWrapper( - title: String, - description: String, - negativeButton: String, - onAuthenticated: () -> Unit, - onCancel: () -> Unit, - onAuthenticationError: () -> Unit, - onAuthenticationSoftError: () -> Unit -) { - val activity = LocalActivity.current as? AppCompatActivity - val context = LocalContext.current - - val executor = remember { ContextCompat.getMainExecutor(context) } - val biometricManager = remember { BiometricManager.from(context) } - - val callback = remember { - object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - - onAuthenticated() - } - - override fun onAuthenticationError( - errCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errCode, errString) - - if (errCode == BiometricPrompt.ERROR_USER_CANCELED || - errCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || - errCode == BiometricPrompt.ERROR_CANCELED - ) { - onCancel() - } else { - context.shortToast(errString.toString()) - onAuthenticationError() - } - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onAuthenticationSoftError() - } - } - } - - val promptInfo = remember { - val secureOption = bestSecureOption(biometricManager) - - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .apply { - if ((secureOption and BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { - setNegativeButtonText(negativeButton) - } - }.setAllowedAuthenticators(secureOption) - .build() - } - - val biometricPrompt = activity?.let { BiometricPrompt(it, executor, callback) } - - DisposableEffect(biometricPrompt) { - biometricPrompt?.authenticate(promptInfo) - onDispose { - biometricPrompt?.cancelAuthentication() - } - } -} - -@Requirement( - "A_21582", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Selection of the best available authentication option on the device." -) -private fun bestSecureOption(biometricManager: BiometricManager): Int { - when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) { - BiometricManager.BIOMETRIC_SUCCESS, - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return BiometricManager.Authenticators.BIOMETRIC_STRONG - } - when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) { - BiometricManager.BIOMETRIC_SUCCESS, - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return BiometricManager.Authenticators.BIOMETRIC_WEAK - } - when (biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)) { - BiometricManager.BIOMETRIC_SUCCESS, - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> return BiometricManager.Authenticators.DEVICE_CREDENTIAL - } - return if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) { - BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK - } else { - BiometricManager.Authenticators.DEVICE_CREDENTIAL - } -} - -// end::BiometricPromptAndBestSecureOption[] diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometryScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometryScreen.kt deleted file mode 100644 index e3aa60a5..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometryScreen.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.userauthentication.ui - -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.navigation.Screen -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod -import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod.Companion.toAuthenticationMode -import de.gematik.ti.erp.app.onboarding.navigation.OnboardingRoutes -import de.gematik.ti.erp.app.onboarding.presentation.rememberOnboardingController -import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode - -class BiometryScreen( - override val navController: NavController, - override val navBackStackEntry: NavBackStackEntry -) : Screen() { - - @Composable - override fun Content() { - val controller = rememberOnboardingController() - - val lazyListState = rememberLazyListState() - val profileName = stringResource(R.string.onboarding_default_profile_name) - val activity = LocalActivity.current - - val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } - - val infoBuilder = biometricPromptBuilder.buildPromptInfo( - title = stringResource(R.string.auth_prompt_headline), - negativeButton = stringResource(R.string.auth_prompt_cancel) - ) - - val prompt = remember(biometricPromptBuilder) { - biometricPromptBuilder.buildBiometricPrompt( - onSuccess = { - controller.onSaveOnboardingData( - authenticationMode = OnboardingSecureAppMethod.DeviceSecurity.toAuthenticationMode(), - profileName = profileName - ) - navController.navigate(OnboardingRoutes.OnboardingAnalyticsPreviewScreen.path()) - } - ) - } - - AnimatedElevationScaffold( - modifier = Modifier.navigationBarsPadding(), - navigationMode = NavigationBarMode.Close, - bottomBar = { - OnboardingBottomBar( - buttonText = stringResource(R.string.settings_device_security_allow), - onButtonClick = { - prompt.authenticate(infoBuilder) - }, - buttonEnabled = true, - info = null, - buttonModifier = Modifier - ) - }, - topBarTitle = stringResource(R.string.settings_biometric_dialog_headline), - listState = lazyListState, - onBack = { navController.popBackStack() } - ) { - LazyColumn( - state = lazyListState, - modifier = Modifier - .wrapContentSize() - .padding( - horizontal = PaddingDefaults.Medium - ) - ) { - item { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .semantics(mergeDescendants = true) {} - ) { - Text( - stringResource(R.string.settings_biometric_dialog_title), - style = AppTheme.typography.h6, - modifier = Modifier.padding( - top = PaddingDefaults.Medium, - bottom = PaddingDefaults.Large - ) - ) - - Text( - text = stringResource(R.string.settings_biometric_dialog_text), - style = AppTheme.typography.body1, - modifier = Modifier.padding( - bottom = PaddingDefaults.Small - ) - ) - } - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt deleted file mode 100644 index 9c1e88f4..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.userauthentication.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod -import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver -import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationStateData.AuthenticationState -import kotlinx.coroutines.flow.map -import org.kodein.di.compose.rememberInstance - -class AuthenticationController( - private val inactivityTimeoutObserver: InactivityTimeoutObserver -) { - private val authenticationFlow = - inactivityTimeoutObserver.authenticationModeAndMethod - .map { - when (it) { - AuthenticationModeAndMethod.None, - AuthenticationModeAndMethod.Authenticated -> AuthenticationState( - SettingsData.AuthenticationMode.Unspecified, - 0 - ) - - is AuthenticationModeAndMethod.AuthenticationRequired -> AuthenticationState( - it.method, - it.nrOfFailedAuthentications - ) - } - } - - val authenticationState - @Composable - get() = authenticationFlow.collectAsState(AuthenticationStateData.defaultAuthenticationState) - - suspend fun isPasswordValid(password: String): Boolean = - inactivityTimeoutObserver.isPasswordValid(password) - - suspend fun onAuthenticated() { - inactivityTimeoutObserver.resetNumberOfAuthenticationFailures() - inactivityTimeoutObserver.authenticated() - } - - suspend fun onFailedAuthentication() = inactivityTimeoutObserver.incrementNumberOfAuthenticationFailures() -} - -@Composable -fun rememberAuthenticationController(): AuthenticationController { - val inactivityTimeoutObserver by rememberInstance() - return remember { AuthenticationController(inactivityTimeoutObserver) } -} - -object AuthenticationStateData { - @Immutable - data class AuthenticationState( - val authenticationMethod: SettingsData.AuthenticationMode, - val nrOfAuthFailures: Int - ) - - val defaultAuthenticationState = AuthenticationState( - authenticationMethod = SettingsData.AuthenticationMode.Unspecified, - nrOfAuthFailures = 0 - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt deleted file mode 100644 index 2f401e7d..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.userauthentication.ui - -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.LockOpen -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.features.BuildConfig -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.PaddingDefaults.Medium -import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder -import de.gematik.ti.erp.app.utils.compose.AlertDialog -import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText -import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton -import de.gematik.ti.erp.app.utils.compose.PasswordTextField -import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.SpacerTiny -import de.gematik.ti.erp.app.utils.compose.annotatedLinkString -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import kotlinx.coroutines.launch -import java.util.Locale - -@Suppress("LongMethod") -@Composable -fun UserAuthenticationScreen() { - val activity = LocalActivity.current - - val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } - - // clear underlying text input focus - val focusManager = LocalFocusManager.current - val scope = rememberCoroutineScope() - - val authentication = rememberAuthenticationController() - val authenticationState by authentication.authenticationState - val navBarInsetsPadding = WindowInsets.systemBars.asPaddingValues() - - var showAuthPrompt by remember { mutableStateOf(false) } - var showError by remember { mutableStateOf(false) } - var initiallyHandledAuthPrompt by rememberSaveable { mutableStateOf(false) } - - val infoBuilder = biometricPromptBuilder.buildPromptInfo( - title = stringResource(R.string.auth_prompt_headline), - negativeButton = stringResource(R.string.auth_prompt_cancel) - ) - - val prompt = remember(biometricPromptBuilder) { - biometricPromptBuilder.buildBiometricPrompt( - onSuccess = { - scope.launch { authentication.onAuthenticated() } - showAuthPrompt = false - }, - onFailure = { - scope.launch { authentication.onFailedAuthentication() } - showAuthPrompt = false - }, - onError = { - showAuthPrompt = false - showError = true - } - ) - } - - LaunchedEffect(showAuthPrompt) { - activity.lifecycleScope.launch { - activity.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { - if (authenticationState.authenticationMethod !is SettingsData.AuthenticationMode.Password && - showAuthPrompt - ) { - prompt.authenticate(infoBuilder) - } - } - } - } - - LaunchedEffect(Unit) { - focusManager.clearFocus(true) - if (!initiallyHandledAuthPrompt && authenticationState.nrOfAuthFailures == 0) { - showAuthPrompt = true - } - initiallyHandledAuthPrompt = true - } - - Scaffold { - Column( - modifier = Modifier - .padding(it) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .then( - when { - navBarInsetsPadding.calculateBottomPadding() <= Medium -> Modifier.statusBarsPadding() - else -> Modifier.systemBarsPadding() - } - ) - ) { - Row( - modifier = Modifier - .padding(top = Medium) - .padding(horizontal = Medium) - .align(Alignment.Start), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painterResource(R.drawable.ic_onboarding_logo_flag), - null, - modifier = Modifier.padding(end = 10.dp) - ) - Icon( - painterResource(R.drawable.ic_onboarding_logo_gematik), - null, - tint = AppTheme.colors.primary900 - ) - } - - AnimatedVisibility( - visible = authenticationState.nrOfAuthFailures > 0 - ) { - AuthenticationHintCard(state = authenticationState) - } - AnimatedVisibility( - visible = authenticationState.nrOfAuthFailures == 0 - ) { - Spacer(modifier = Modifier.height(80.dp)) - } - - AnimatedVisibility(visible = showError) { - AuthenticationScreenErrorContent( - showAuthPromptOnClick = { showAuthPrompt = true } - ) - } - - AnimatedVisibility(visible = !showError) { - AuthenticationScreenContent( - showAuthPromptOnClick = { showAuthPrompt = true } - ) - } - - Spacer(modifier = Modifier.weight(1f)) - if (showError) { - AuthenticationScreenErrorBottomContent( - state = authenticationState - ) - } else { - Image( - painterResource(R.drawable.crew), - null, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Medium), - contentScale = ContentScale.FillWidth - ) - } - } - } - - if (showAuthPrompt && authenticationState.authenticationMethod is SettingsData.AuthenticationMode.Password) { - PasswordPrompt( - authentication, - onAuthenticated = { - showAuthPrompt = false - scope.launch { authentication.onAuthenticated() } - }, - onCancel = { - showAuthPrompt = false - }, - onAuthenticationError = { - scope.launch { authentication.onFailedAuthentication() } - showAuthPrompt = false - showError = true - } - ) - } -} - -@Composable -private fun AuthenticationScreenErrorContent( - showAuthPromptOnClick: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(80.dp)) - Image( - painterResource(R.drawable.woman_red_shirt_circle_red), - null, - alignment = Alignment.Center - ) - SpacerMedium() - Text( - stringResource(R.string.auth_subtitle_error), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.auth_info_error), - textAlign = TextAlign.Center, - style = AppTheme.typography.body1l - ) - SpacerLarge() - PrimaryButton( - onClick = showAuthPromptOnClick, - elevation = ButtonDefaults.elevation(8.dp), - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues( - horizontal = PaddingDefaults.Large, - vertical = PaddingDefaults.ShortMedium - ) - ) { - Icon(Icons.Rounded.LockOpen, null) - SpacerTiny() - SpacerSmall() - Text(stringResource(R.string.auth_button)) - } - } -} - -@Composable -private fun AuthenticationScreenErrorBottomContent(state: AuthenticationStateData.AuthenticationState) { - Column( - modifier = Modifier - .background(color = AppTheme.colors.neutral100) - .padding( - bottom = PaddingDefaults.Large, - start = Medium, - end = Medium, - top = Medium - ) - .fillMaxWidth() - ) { - val uriHandler = LocalUriHandler.current - val link = annotatedLinkString( - stringResource(R.string.auth_link_to_gematik_q_and_a), - stringResource(R.string.auth_link_to_gematik_helptext) - ) - when (state.authenticationMethod) { - SettingsData.AuthenticationMode.DeviceSecurity -> - Text( - text = stringResource(R.string.auth_failed_biometry_info), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - - else -> - ClickableTaggedText( - annotatedStringResource(R.string.auth_failed_password_info, link), - style = AppTheme.typography.body2l.merge(TextStyle(textAlign = TextAlign.Center)), - onClick = { range -> - uriHandler.openUri(range.item) - } - ) - } - } -} - -@Composable -private fun AuthenticationScreenContent( - showAuthPromptOnClick: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(R.string.auth_headline), - style = AppTheme.typography.h5, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - SpacerMedium() - Text( - stringResource(R.string.auth_subtitle), - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - SpacerSmall() - Text( - stringResource(R.string.auth_information), - textAlign = TextAlign.Center, - style = AppTheme.typography.body1l - ) - SpacerLarge() - PrimaryButton( - onClick = showAuthPromptOnClick, - elevation = ButtonDefaults.elevation(8.dp), - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues( - horizontal = PaddingDefaults.Large, - vertical = PaddingDefaults.ShortMedium - ) - ) { - Icon(Icons.Rounded.LockOpen, null) - SpacerTiny() - SpacerSmall() - Text(stringResource(R.string.auth_button)) - } - } -} - -@Composable -private fun PasswordPrompt( - authenticationState: AuthenticationController, - onAuthenticated: () -> Unit, - onCancel: () -> Unit, - onAuthenticationError: () -> Unit -) { - var password by remember { mutableStateOf("") } - val coroutineScope = rememberCoroutineScope() - - AlertDialog( - onDismissRequest = onCancel, - buttons = { - if (BuildKonfig.INTERNAL && BuildConfig.DEBUG) { - OutlinedDebugButton("SKIP", onClick = onAuthenticated) - } - TextButton( - onClick = onCancel - ) { - Text(stringResource(R.string.cancel).uppercase(Locale.getDefault())) - } - TextButton( - enabled = password.isNotEmpty(), - onClick = { - coroutineScope.launch { - if (authenticationState.isPasswordValid(password)) { - onAuthenticated() - } else { - onAuthenticationError() - } - } - } - ) { - Text(stringResource(R.string.auth_prompt_check_password).uppercase(Locale.getDefault())) - } - }, - title = null, - text = { - PasswordTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 56.dp), - value = password, - onValueChange = { - password = it - }, - allowAutofill = true, - allowVisiblePassword = true, - label = { - Text(stringResource(R.string.auth_prompt_enter_password)) - }, - onSubmit = {} - ) - } - ) -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/AuthenticationHintCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/AuthenticationHintCard.kt new file mode 100644 index 00000000..a8fbbc71 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/AuthenticationHintCard.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT +import androidx.biometric.BiometricPrompt.ERROR_LOCKOUT_PERMANENT +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData +import de.gematik.ti.erp.app.utils.compose.HintCard +import de.gematik.ti.erp.app.utils.compose.HintCardDefaults +import de.gematik.ti.erp.app.utils.compose.HintSmallImage +import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource + +@Composable +fun AuthenticationHintCard( + state: AuthenticationStateData.AuthenticationState +) { + Box( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) + ) { + HintCard( + modifier = Modifier.padding(vertical = PaddingDefaults.Medium), + properties = HintCardDefaults.flatProperties( + backgroundColor = AppTheme.colors.red100 + ), + image = { + HintSmallImage( + painterResource(R.drawable.oh_no_girl_hint_red), + innerPadding = it + ) + }, + title = { Text(stringResource(R.string.auth_error_failed_auths_headline)) }, + body = { + Text( + buildAnnotatedString { + append( + annotatedPluralsResource( + R.plurals.auth_error_failed_auths_info, + state.authentication.failedAuthenticationAttempts, + AnnotatedString(state.authentication.failedAuthenticationAttempts.toString()) + ) + ) + state.biometricError?.let { + if ( + it.code == ERROR_LOCKOUT || + it.code == ERROR_LOCKOUT_PERMANENT + ) { + append(" " + it.name) + } + } + } + ) + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/GematikLogo.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/GematikLogo.kt new file mode 100644 index 00000000..5ed325e6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/GematikLogo.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton + +@Composable +internal fun GematikLogo( + onSkipAuthentication: () -> Unit +) { + Row( + modifier = Modifier + .padding(top = PaddingDefaults.Medium) + .padding(horizontal = PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Image( + painterResource(R.drawable.ic_onboarding_logo_flag), + null, + modifier = Modifier.padding(end = SizeDefaults.oneQuarter) + ) + Icon( + painterResource(R.drawable.ic_onboarding_logo_gematik), + null, + tint = AppTheme.colors.primary900 + ) + if (BuildKonfig.INTERNAL && BuildConfig.DEBUG) { + Spacer(modifier = Modifier.weight(1f)) + OutlinedDebugButton("SKIP", onClick = onSkipAuthentication) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/PasswordAuthenticationDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/PasswordAuthenticationDialog.kt new file mode 100644 index 00000000..28e65c88 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/PasswordAuthenticationDialog.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog +import de.gematik.ti.erp.app.utils.compose.PasswordTextField + +@Composable +internal fun PasswordAuthenticationDialog( + onChangePassword: (String) -> Unit, + onAuthenticate: () -> Unit, + onCancel: () -> Unit +) { + var password by remember { mutableStateOf("") } + ErezeptAlertDialog( + title = stringResource(id = R.string.auth_password_dialog_title), + body = { + Text( + text = stringResource(id = R.string.auth_password_dialog_body), + modifier = Modifier.fillMaxWidth(), + style = AppTheme.typography.body2 + ) + SpacerMedium() + PasswordTextField( + modifier = Modifier + .testTag("password_prompt_password_field") + .fillMaxWidth() + .heightIn(min = SizeDefaults.sevenfold), + value = password, + onValueChange = { + password = it + }, + allowAutofill = true, + allowVisiblePassword = true, + label = { + Text(stringResource(R.string.auth_password_dialog_label)) + }, + onSubmit = { + onChangePassword(password) + onAuthenticate() + } + ) + }, + onConfirmRequest = { + onChangePassword(password) + onAuthenticate() + }, + onDismissRequest = onCancel + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationBottomSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationBottomSection.kt new file mode 100644 index 00000000..1db2aef3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationBottomSection.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.ui.screens.provideLinkForString +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData +import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid + +@Composable +internal fun UserAuthenticationBottomSection() { + Image( + painterResource(R.drawable.crew), + null, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + contentScale = ContentScale.FillWidth + ) +} + +@Composable +internal fun UserAuthenticationBottomErrorSection( + state: AuthenticationStateData.AuthenticationState +) { + Column( + modifier = Modifier + .background( + color = AppTheme.colors.neutral100 + ) + .padding( + bottom = PaddingDefaults.Large, + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium, + top = PaddingDefaults.Medium + ) + .fillMaxWidth() + ) { + when { + state.authentication.deviceSecurity -> + Text( + text = stringResource(R.string.auth_failed_biometry_info), + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + else -> { + val link = + provideLinkForString( + stringResource(id = R.string.auth_link_to_gematik_helptext), + annotation = stringResource(id = R.string.auth_link_to_gematik_q_and_a), + tag = "URL", + linkColor = AppTheme.colors.primary500 + ) + + val uriHandler = LocalUriHandler.current + + ClickableTaggedText( + annotatedStringResource(R.string.auth_failed_password_error_info, link), + style = AppTheme.typography.body2l, + onClick = { range -> + uriHandler.openUriWhenValid(range.item) + } + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationDataScreenContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationDataScreenContent.kt new file mode 100644 index 00000000..ddd11cab --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationDataScreenContent.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData + +@Composable +internal fun UserAuthenticationDataScreenContent( + contentPadding: PaddingValues, + authenticationState: AuthenticationStateData.AuthenticationState, + onAuthenticate: () -> Unit, + onShowPasswordDialog: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .verticalScroll(rememberScrollState()) + ) { + @Requirement( + "O.Pass_4#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "If the user has failed to authenticate," + + " the number of failed authentication attempts is displayed." + ) + AuthenticationHintCard( + state = authenticationState + ) + Spacer(modifier = Modifier.height(SizeDefaults.tenfold)) + UserAuthenticationLoginSection( + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + Spacer(modifier = Modifier.weight(1f)) + if (authenticationState.authentication.showFailedAuthenticationAttemptsError) { + UserAuthenticationBottomErrorSection(authenticationState) + } else { + UserAuthenticationBottomSection() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationEmptyScreenContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationEmptyScreenContent.kt new file mode 100644 index 00000000..bffd8c2e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationEmptyScreenContent.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData + +@Composable +internal fun UserAuthenticationEmptyScreenContent( + contentPadding: PaddingValues, + authenticationState: AuthenticationStateData.AuthenticationState, + onAuthenticate: () -> Unit, + onShowPasswordDialog: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .verticalScroll(rememberScrollState()) + ) { + Spacer( + modifier = Modifier.height( + SizeDefaults.tenfold + ) + ) + UserAuthenticationLoginSection( + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + Spacer(modifier = Modifier.weight(1f)) + UserAuthenticationBottomSection() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationErrorScreenContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationErrorScreenContent.kt new file mode 100644 index 00000000..aff78514 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationErrorScreenContent.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData + +@Composable +internal fun UserAuthenticationErrorScreenContent( + contentPadding: PaddingValues, + authenticationState: AuthenticationStateData.AuthenticationState, + onAuthenticate: () -> Unit, + onShowPasswordDialog: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + .verticalScroll(rememberScrollState()) + ) { + @Requirement( + "O.Pass_4#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "If the user has failed to authenticate," + + " the number of failed authentication attempts is displayed." + ) + AuthenticationHintCard( + state = authenticationState + ) + Spacer(modifier = Modifier.height(SizeDefaults.tenfold)) + UserAuthenticationLoginSection( + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + Spacer(modifier = Modifier.weight(1f)) + UserAuthenticationBottomErrorSection( + state = authenticationState + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationLoginSection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationLoginSection.kt new file mode 100644 index 00000000..799bb3d4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/components/UserAuthenticationLoginSection.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.LockOpen +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData +import de.gematik.ti.erp.app.utils.SpacerLarge +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerShortMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.PrimaryButton + +@Composable +internal fun UserAuthenticationLoginSection( + authenticationState: AuthenticationStateData.AuthenticationState, + onAuthenticate: () -> Unit, + onShowPasswordDialog: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + stringResource(R.string.auth_title), + style = AppTheme.typography.h5, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + SpacerTiny() + Text( + stringResource(R.string.auth_body), + style = AppTheme.typography.body1l, + textAlign = TextAlign.Center + ) + SpacerLarge() + + PrimaryButton( + onClick = onAuthenticate, + elevation = ButtonDefaults.elevation(SizeDefaults.one), + shape = RoundedCornerShape(SizeDefaults.one), + contentPadding = PaddingValues( + horizontal = PaddingDefaults.Large, + vertical = PaddingDefaults.ShortMedium + ) + ) { + Icon(Icons.Rounded.LockOpen, null) + SpacerShortMedium() + Text( + if (authenticationState.authentication.failedAuthenticationAttempts > 0) { + stringResource(R.string.auth_retry_button) + } else { + stringResource(R.string.auth_button) + } + ) + } + + SpacerMedium() + if ( + authenticationState.authentication.bothMethodsAvailable + ) { + TextButton(onClick = onShowPasswordDialog) { + Text( + text = stringResource(id = R.string.auth_alternative_button), + textAlign = TextAlign.Center + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/preview/UserAuthenticationPreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/preview/UserAuthenticationPreviewParameterProvider.kt new file mode 100644 index 00000000..f6d54c2d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/preview/UserAuthenticationPreviewParameterProvider.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.preview + +import androidx.biometric.BiometricPrompt +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData +import de.gematik.ti.erp.app.utils.uistate.UiState + +data class UserAuthenticationPreviewParameter( + val name: String, + val authenticationState: AuthenticationStateData.AuthenticationState, + val uiState: UiState +) + +val AuthMethodBiometry = SettingsData.Authentication( + deviceSecurity = true, + failedAuthenticationAttempts = 0, + password = null +) +val AuthMethodPassword = SettingsData.Authentication( + deviceSecurity = false, + failedAuthenticationAttempts = 0, + password = SettingsData.Authentication.Password("password") +) +val AuthMethodBoth = SettingsData.Authentication( + deviceSecurity = true, + failedAuthenticationAttempts = 0, + password = SettingsData.Authentication.Password("password") +) +val AuthMethodBiometryError = SettingsData.Authentication( + deviceSecurity = true, + failedAuthenticationAttempts = 5, + password = null +) +val AuthMethodPasswordError = SettingsData.Authentication( + deviceSecurity = false, + failedAuthenticationAttempts = 5, + password = SettingsData.Authentication.Password("password") +) +val AuthMethodBothError = SettingsData.Authentication( + deviceSecurity = true, + failedAuthenticationAttempts = 5, + password = SettingsData.Authentication.Password("password") +) +val biometricErrorLockOut = AuthenticationStateData.BiometricError( + "biometric error lock out", + BiometricPrompt.ERROR_LOCKOUT +) +val biometricErrorLockOutPermanent = AuthenticationStateData.BiometricError( + "biometric error lock out permanent", + BiometricPrompt.ERROR_LOCKOUT_PERMANENT +) + +class UserAuthenticationPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + UserAuthenticationPreviewParameter( + name = "UiStateNoErrorBiometry", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBiometry + ), + uiState = UiState.Empty() + ), + UserAuthenticationPreviewParameter( + name = "UiStateNoErrorPassword", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodPassword + ), + uiState = UiState.Empty() + ), + UserAuthenticationPreviewParameter( + name = "UiStateNoErrorBoth", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBoth + ), + uiState = UiState.Empty() + ), + UserAuthenticationPreviewParameter( + name = "UiStateErrorBiometryLockOut", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBiometryError, + biometricError = biometricErrorLockOut + ), + uiState = UiState.Error( + AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBiometryError, + biometricError = biometricErrorLockOut + ) + ) + ), + UserAuthenticationPreviewParameter( + name = "UiStateErrorBothLockOutPermanent", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBothError, + biometricError = biometricErrorLockOutPermanent + ), + uiState = UiState.Error( + AuthenticationStateData.AuthenticationState( + authentication = AuthMethodBothError, + biometricError = biometricErrorLockOutPermanent + ) + ) + ), + UserAuthenticationPreviewParameter( + name = "UiStateErrorPassword", + authenticationState = AuthenticationStateData.AuthenticationState( + authentication = AuthMethodPasswordError + ), + uiState = UiState.Error( + AuthenticationStateData.AuthenticationState( + authentication = AuthMethodPasswordError + ) + ) + ) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/screens/UserAuthenticationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/screens/UserAuthenticationScreen.kt new file mode 100644 index 00000000..d7923943 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/screens/UserAuthenticationScreen.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.userauthentication.navigation.UserAuthenticationRoutes +import de.gematik.ti.erp.app.userauthentication.presentation.AuthenticationStateData +import de.gematik.ti.erp.app.userauthentication.presentation.rememberAuthenticationController +import de.gematik.ti.erp.app.userauthentication.ui.components.GematikLogo +import de.gematik.ti.erp.app.userauthentication.ui.components.PasswordAuthenticationDialog +import de.gematik.ti.erp.app.userauthentication.ui.components.UserAuthenticationDataScreenContent +import de.gematik.ti.erp.app.userauthentication.ui.components.UserAuthenticationErrorScreenContent +import de.gematik.ti.erp.app.userauthentication.ui.components.UserAuthenticationEmptyScreenContent +import de.gematik.ti.erp.app.userauthentication.ui.preview.UserAuthenticationPreviewParameter +import de.gematik.ti.erp.app.userauthentication.ui.preview.UserAuthenticationPreviewParameterProvider +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.UiStateMachine +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.uistate.UiState + +class UserAuthenticationScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry +) : Screen() { + + @Composable + override fun Content() { + val focusManager = LocalFocusManager.current + val dialog = LocalDialog.current + + val authenticationController = rememberAuthenticationController() + val authenticationState by authenticationController.authenticationState.collectAsStateWithLifecycle() + val uiState by authenticationController.uiState.collectAsStateWithLifecycle(UiState.Loading()) + + val authenticationEvent = authenticationController.authenticationWithPasswordEvent + + val onLeaveUserAuthenticationScreen: () -> Unit = { + navController.popBackStack( + UserAuthenticationRoutes.subGraphName(), + inclusive = true + ) + } + + LaunchedEffect(Unit) { + if (authenticationState.authentication.methodIsUnspecified) { + onLeaveUserAuthenticationScreen() // leave screen if no authentication is required + } + } + + AuthenticationWithPasswordEventListener( + event = authenticationEvent, + focusManager = focusManager, + dialog = dialog, + onChangePassword = { authenticationController.onChangePassword(password = it) }, + onAuthenticateWithPassword = { + authenticationController.onAuthenticateWithPassword( + onSuccessLeaveAuthScreen = onLeaveUserAuthenticationScreen + ) + } + ) + + BackHandler {} // override back swiping to not skip the auth process + + UserAuthenticationScreenScaffold( + authenticationState = authenticationState, + uiState = uiState, + onSkipAuthentication = { + authenticationController.onSuccessfulAuthentication { + onLeaveUserAuthenticationScreen() + } + }, + onShowPasswordDialog = { authenticationEvent.trigger(Unit) }, + onAuthenticate = { + authenticationController.onClickAuthenticate( + onSuccessLeaveAuthScreen = onLeaveUserAuthenticationScreen + ) + } + ) + } +} + +@Composable +private fun UserAuthenticationScreenScaffold( + authenticationState: AuthenticationStateData.AuthenticationState, + uiState: UiState, + onSkipAuthentication: () -> Unit, + onShowPasswordDialog: () -> Unit, + onAuthenticate: () -> Unit +) { + Scaffold( + topBar = { + GematikLogo { + onSkipAuthentication() + } + } + ) { innerPadding -> + UiStateMachine( + state = uiState, + onLoading = { + Center { + CircularProgressIndicator() + } + }, + onEmpty = { + UserAuthenticationEmptyScreenContent( + contentPadding = innerPadding, + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + }, + onError = { + UserAuthenticationErrorScreenContent( + contentPadding = innerPadding, + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + }, + onContent = { + UserAuthenticationDataScreenContent( + contentPadding = innerPadding, + authenticationState = authenticationState, + onAuthenticate = onAuthenticate, + onShowPasswordDialog = onShowPasswordDialog + ) + } + ) + } +} + +@Composable +fun AuthenticationWithPasswordEventListener( + event: ComposableEvent, + focusManager: FocusManager, + dialog: DialogScaffold, + onChangePassword: (String) -> Unit, + onAuthenticateWithPassword: () -> Unit +) { + event.listen { + focusManager.clearFocus(true) + dialog.show { + PasswordAuthenticationDialog( + onChangePassword = onChangePassword, + onAuthenticate = { + onAuthenticateWithPassword() + it.dismiss() + }, + onCancel = { + it.dismiss() + } + ) + } + } +} + +@LightDarkPreview +@Composable +fun UserAuthenticationScreenPreview( + @PreviewParameter(UserAuthenticationPreviewParameterProvider::class) previewData: UserAuthenticationPreviewParameter +) { + PreviewAppTheme { + UserAuthenticationScreenScaffold( + authenticationState = previewData.authenticationState, + uiState = previewData.uiState, + onSkipAuthentication = {}, + onAuthenticate = {}, + onShowPasswordDialog = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/MailClient.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/MailClient.kt index 67384502..2111adb6 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/MailClient.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/MailClient.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AccessToCameraDenied.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AccessToCameraDenied.kt new file mode 100644 index 00000000..32263ec4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AccessToCameraDenied.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import android.provider.Settings +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material3.OutlinedButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.base.openSettingsAsNewActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Suppress("MagicNumber") +@Composable +fun AccessToCameraDenied( + showSettingsButton: Boolean = false, + showTopBar: Boolean = true, + onClick: () -> Unit +) { + val context = LocalContext.current + Surface( + color = Color.Black, + contentColor = Color.White, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .testTag("camera/disallowed") + ) { + if (showTopBar) { + CameraTopBar( + flashEnabled = false, + onClickClose = onClick, + onFlashClick = {} + ) + Spacer(Modifier.weight(0.4f)) + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + Icons.Rounded.ErrorOutline, + null, + modifier = Modifier.size(48.dp) + ) + Text( + stringResource(R.string.cam_access_denied_headline), + style = AppTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Text( + stringResource(R.string.cam_access_denied_description), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + if (showSettingsButton) { + SpacerMedium() + OutlinedButton( + colors = erezeptButtonColors(), + onClick = { + context.openSettingsAsNewActivity(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + } + ) { + androidx.compose.material3.Text(stringResource(R.string.open_settings)) + } + } + } + Spacer(Modifier.weight(0.6f)) + } + } +} + +@LightDarkPreview +@Composable +internal fun AccessToCameraDeniedPreview() { + PreviewAppTheme { + AccessToCameraDenied( + onClick = {} + ) + } +} + +@LightDarkPreview +@Composable +internal fun AccessToCameraDeniedWithButtonPreview() { + PreviewAppTheme { + AccessToCameraDenied( + showSettingsButton = true, + showTopBar = false, + onClick = {} + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimateComposable.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimateComposable.kt deleted file mode 100644 index df0aacf9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimateComposable.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils.compose - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterTransition -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.navigation.slideInAnimation -import kotlinx.coroutines.delay - -@Composable -fun AnimateComposable( - delayTime: Long = 350, - enterTransition: EnterTransition = slideInAnimation(delayTime), - content: @Composable () -> Unit -) { - var isVisible by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - delay(delayTime) - isVisible = true - } - - AnimatedVisibility( - visible = isVisible, - enter = enterTransition - ) { - content() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt index 1a33834f..ee5b235f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt @@ -1,32 +1,36 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.AppBarDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.ScaffoldState import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -34,128 +38,137 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults @Composable fun AnimatedElevationScaffold( - modifier: Modifier = Modifier, - scaffoldState: ScaffoldState = rememberScaffoldState(), - topBarColor: Color = MaterialTheme.colors.surface, - navigationMode: NavigationBarMode = NavigationBarMode.Close, - bottomBar: @Composable () -> Unit = {}, - topBarTitle: String, - listState: LazyListState, - onBack: () -> Unit, + topBar: @Composable (Boolean) -> Unit, + listState: LazyListState = rememberLazyListState(), content: @Composable (PaddingValues) -> Unit ) { + val activity = LocalActivity.current as? BaseActivity + val padding = remember { activity?.applicationInnerPadding } val elevated by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 } } Scaffold( - modifier = modifier, - scaffoldState = scaffoldState, topBar = { - NavigationTopAppBar( - navigationMode = navigationMode, - backgroundColor = topBarColor, - title = topBarTitle, - elevation = if (elevated) { - AppBarDefaults.TopAppBarElevation - } else { - SizeDefaults.zero - }, - onBack = onBack - ) + Surface( + elevation = when { + elevated -> SizeDefaults.half + else -> SizeDefaults.zero + } + ) { + topBar(elevated) + } }, - bottomBar = bottomBar, - content = content + content = { innerPadding -> + content(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) + } ) } @Composable fun AnimatedElevationScaffold( - modifier: Modifier = Modifier, - scaffoldState: ScaffoldState = rememberScaffoldState(), - topBarColor: Color = MaterialTheme.colors.surface, - navigationMode: NavigationBarMode? = NavigationBarMode.Close, - bottomBar: @Composable () -> Unit = {}, - topBarTitle: String, - listState: LazyListState, - onBack: () -> Unit, - snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, - actions: @Composable RowScope.() -> Unit, + listState: LazyListState = rememberLazyListState(), + topBar: @Composable () -> Unit, + floatingActionButton: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit ) { + val activity = LocalActivity.current as? BaseActivity + val padding = remember { activity?.applicationInnerPadding } val elevated by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 } } Scaffold( - modifier = modifier, - scaffoldState = scaffoldState, topBar = { - NavigationTopAppBar( - navigationMode = navigationMode, - backgroundColor = topBarColor, - title = topBarTitle, - elevation = if (elevated) { - AppBarDefaults.TopAppBarElevation - } else { - SizeDefaults.zero - }, - onBack = onBack, - actions = actions - ) + Surface( + elevation = when { + elevated -> AppBarDefaults.TopAppBarElevation + else -> SizeDefaults.zero + } + ) { + topBar() + } }, - snackbarHost = snackbarHost, - bottomBar = bottomBar, - content = content + floatingActionButton = { + floatingActionButton() + }, + content = { innerPadding -> + content(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) + } ) } @Composable fun AnimatedElevationScaffold( modifier: Modifier = Modifier, + topBarTitle: String, + topBarPadding: PaddingValues = PaddingValues.Absolute( + SizeDefaults.zero, + SizeDefaults.zero, + SizeDefaults.zero, + SizeDefaults.zero + ), + scaffoldState: ScaffoldState = rememberScaffoldState(), topBarColor: Color = MaterialTheme.colors.surface, navigationMode: NavigationBarMode? = NavigationBarMode.Close, + listState: LazyListState = rememberLazyListState(), + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, bottomBar: @Composable () -> Unit = {}, - topBarTitle: String, - elevated: Boolean, + actions: @Composable RowScope.() -> Unit = {}, onBack: () -> Unit, - actions: @Composable (RowScope.() -> Unit), content: @Composable (PaddingValues) -> Unit ) { - val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else SizeDefaults.zero } - + val activity = LocalActivity.current as? BaseActivity + val padding = remember { activity?.applicationInnerPadding } + val elevated by remember { + derivedStateOf { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 } + } Scaffold( modifier = modifier, + scaffoldState = scaffoldState, topBar = { NavigationTopAppBar( + modifier = Modifier.padding(topBarPadding), navigationMode = navigationMode, backgroundColor = topBarColor, title = topBarTitle, - elevation = elevation, + elevation = when { + elevated -> AppBarDefaults.TopAppBarElevation + else -> SizeDefaults.zero + }, onBack = onBack, actions = actions ) }, + snackbarHost = snackbarHost, bottomBar = bottomBar, - content = content + content = { innerPadding -> + content(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) + } ) } +// AnimatedElevationScaffold used it titles @Composable fun AnimatedElevationScaffold( modifier: Modifier = Modifier, + topBarTitle: String, scaffoldState: ScaffoldState = rememberScaffoldState(), topBarColor: Color = MaterialTheme.colors.surface, - navigationMode: NavigationBarMode = NavigationBarMode.Close, + listState: LazyListState = rememberLazyListState(), + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, + actions: @Composable RowScope.() -> Unit = {}, bottomBar: @Composable () -> Unit = {}, - topBarTitle: String, - listState: LazyListState, - onBack: () -> Unit, - snackbarHost: @Composable (SnackbarHostState) -> Unit, content: @Composable (PaddingValues) -> Unit ) { + val activity = LocalActivity.current as? BaseActivity + val padding = remember { activity?.applicationInnerPadding } val elevated by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 } } @@ -163,20 +176,54 @@ fun AnimatedElevationScaffold( modifier = modifier, scaffoldState = scaffoldState, topBar = { - NavigationTopAppBar( - navigationMode = navigationMode, - backgroundColor = topBarColor, - title = topBarTitle, - elevation = if (elevated) { - AppBarDefaults.TopAppBarElevation - } else { - SizeDefaults.zero + TopAppBar( + title = { + Text( + modifier = Modifier.padding( + top = PaddingDefaults.Medium + ), + text = topBarTitle, + style = AppTheme.typography.h5 + ) }, - onBack = onBack + actions = actions, + backgroundColor = topBarColor, + elevation = when { + elevated -> AppBarDefaults.TopAppBarElevation + else -> SizeDefaults.zero + } ) }, - bottomBar = bottomBar, snackbarHost = snackbarHost, - content = content + bottomBar = bottomBar, + content = { innerPadding -> + content(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) + } ) } + +// switch between AnimatedElevationScaffold with back button and different text +// and the one used for titles +@Composable +fun AnimatedElevationScaffold( + topBarTitle: String, + listState: LazyListState, + isModalFlow: Boolean, + onBack: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + if (isModalFlow) { + AnimatedElevationScaffold( + topBarTitle = topBarTitle, + listState = listState, + onBack = onBack, + content = content + ) + } else { + AnimatedElevationScaffold( + topBarTitle = topBarTitle, + listState = listState, + content = content + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffoldWithScrollState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffoldWithScrollState.kt new file mode 100644 index 00000000..c9980322 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffoldWithScrollState.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.theme.SizeDefaults + +// This AnimatedElevationScaffoldWithScrollState is used when the scroll state is fed from the child composable +@Composable +fun AnimatedElevationScaffoldWithScrollState( + modifier: Modifier = Modifier, + topBarTitle: String, + scaffoldState: ScaffoldState = rememberScaffoldState(), + topBarColor: Color = MaterialTheme.colors.surface, + navigationMode: NavigationBarMode? = NavigationBarMode.Close, + elevated: Boolean, + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, + bottomBar: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + onBack: () -> Unit, + content: @Composable (PaddingValues) -> Unit +) { + val padding = (LocalActivity.current as? BaseActivity)?.applicationInnerPadding + + Scaffold( + modifier = modifier, + scaffoldState = scaffoldState, + topBar = { + NavigationTopAppBar( + navigationMode = navigationMode, + backgroundColor = topBarColor, + title = topBarTitle, + elevation = when { + elevated -> AppBarDefaults.TopAppBarElevation + else -> SizeDefaults.zero + }, + onBack = onBack, + actions = actions + ) + }, + snackbarHost = snackbarHost, + bottomBar = bottomBar, + content = { innerPadding -> + content(padding?.combineWithInnerScaffold(innerPadding) ?: innerPadding) + } + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt index dcbbde71..82f0c0d9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.utils.compose import androidx.compose.animation.AnimatedVisibility diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Banner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Banner.kt new file mode 100644 index 00000000..10458ca9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Banner.kt @@ -0,0 +1,346 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("UnusedPrivateMember") + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Scaffold +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.letNotNullOnCondition + +private const val MIN_WEIGHT = 0.1f +private const val MAX_WEIGHT = 0.9f +private const val INFO_WEIGHT = 0.15f + +sealed class BannerIcon( + open val vector: ImageVector?, + open val color: Color? +) { + data object NoIcon : BannerIcon(null, null) + data object Gears : BannerIcon(Icons.Default.Settings, Color.White) + data object Info : BannerIcon(Icons.Default.Info, Color.White) + data object Warning : BannerIcon(Icons.Default.Warning, Color.Black) + data class Custom(override val vector: ImageVector, override val color: Color) : BannerIcon(vector, color) +} + +data class BannerClickableIcon( + val icon: BannerIcon, + val onClick: (() -> Unit)? +) + +data class BannerClickableTextIcon( + val text: String?, + val icon: BannerIcon, + val onClick: (() -> Unit)? +) + +@Composable +fun Banner( + modifier: Modifier = Modifier, + title: String? = null, + text: String, + contentColor: Color = Color.White, + containerColor: Color = AppTheme.colors.primary300, + borderColor: Color = AppTheme.colors.primary500, + startIcon: BannerClickableIcon = BannerClickableIcon(BannerIcon.NoIcon, null), + gearsIcon: BannerClickableIcon = BannerClickableIcon(BannerIcon.NoIcon, null), + bottomIcon: BannerClickableTextIcon = BannerClickableTextIcon(null, BannerIcon.NoIcon, null), + onClickClose: (() -> Unit)? = null +) { + Card( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(SizeDefaults.double)) + .border( + border = BorderStroke(SizeDefaults.eighth, borderColor), + shape = RoundedCornerShape(SizeDefaults.double) + ), + colors = CardDefaults.cardColors( + containerColor = containerColor, + contentColor = contentColor + ), + elevation = CardDefaults.cardElevation(SizeDefaults.half) + ) { + val isNoStartIcon = startIcon.icon == BannerIcon.NoIcon + val isNoGearsIcon = gearsIcon.icon == BannerIcon.NoIcon + val isNoBottomIcon = bottomIcon.icon == BannerIcon.NoIcon + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = PaddingDefaults.Small) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween + ) { + title?.let { + SpacerSmall() + ErezeptText.SubtitleOne( + modifier = Modifier + .padding(horizontal = PaddingDefaults.MediumPlus) + .weight(MAX_WEIGHT), + color = AppTheme.colors.primary600, + text = title + ) + } + onClickClose?.let { + IconButton( + modifier = Modifier + .fillMaxWidth() + .weight(MIN_WEIGHT) + .padding( + end = PaddingDefaults.ShortMedium + ) + .size(SizeDefaults.triple), + onClick = it + ) { + Icon(imageVector = Icons.Default.Close, contentDescription = "Close") + } + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .let { + when { + isNoStartIcon -> it.padding(horizontal = PaddingDefaults.Medium) + else -> it.padding(end = PaddingDefaults.Medium) + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + letNotNullOnCondition( + first = startIcon.icon.vector, + second = startIcon.icon.color, + condition = { !isNoStartIcon && startIcon.onClick != null } + ) { vector, color -> + IconButton( + modifier = Modifier.weight(INFO_WEIGHT), + onClick = { startIcon.onClick?.invoke() } + ) { + Icon( + imageVector = vector, + tint = color, + contentDescription = "Info" + ) + } + } + Text( + modifier = Modifier.weight(MAX_WEIGHT), + style = AppTheme.typography.body1, + text = text + ) + letNotNullOnCondition( + first = gearsIcon.icon.vector, + second = gearsIcon.icon.color, + condition = { !isNoGearsIcon && gearsIcon.onClick != null } + ) { vector, color -> + IconButton( + modifier = Modifier + .weight(MIN_WEIGHT) + .size(SizeDefaults.triple), + onClick = { gearsIcon.onClick?.invoke() } + ) { + Icon( + imageVector = vector, + tint = color, + contentDescription = "Close" + ) + } + } + } + letNotNullOnCondition( + first = bottomIcon.text, + second = bottomIcon.icon.vector, + third = bottomIcon.icon.color, + condition = { !isNoBottomIcon && bottomIcon.onClick != null } + ) { text, vector, color -> + TextButton( + modifier = Modifier + .testTag(TestTag.AlertDialog.ConfirmButton) + .padding(horizontal = PaddingDefaults.Small), + onClick = { bottomIcon.onClick?.invoke() } + ) { + ErezeptText.Body(text = text, color = color) + SpacerTiny() + Icon( + imageVector = vector, + tint = color, + contentDescription = "ArrowForward" + ) + } + } + } + } +} + +@Composable +fun SimpleBanner( + text: String, + modifier: Modifier = Modifier, + contentColor: Color = AppTheme.colors.neutral800, + containerColor: Color = AppTheme.colors.primary300 +) { + Row( + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .background(containerColor), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + ErezeptText.Body( + modifier = Modifier.padding(vertical = PaddingDefaults.Small), + color = contentColor, + text = text + ) + } +} + +@LightDarkPreview +@Composable +fun SimpleBannerPreview() { + PreviewAppTheme { + Scaffold { paddingValues -> + SimpleBanner( + modifier = Modifier.padding(paddingValues).padding(top = PaddingDefaults.Tiny), + containerColor = AppTheme.colors.neutral200, + text = stringResource(R.string.no_internet_text) + ) + } + } +} + +@LightDarkPreview +@Composable +private fun AllIconsBannerPreview() { + PreviewAppTheme { + Banner( + title = "Lorem ipsum dolor sit amet", + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + + "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et", + startIcon = BannerClickableIcon(BannerIcon.Info) {}, + gearsIcon = BannerClickableIcon(BannerIcon.Gears) {}, + bottomIcon = BannerClickableTextIcon( + text = "Lorem ipsum dolor sit amet", + icon = BannerIcon.Custom(Icons.AutoMirrored.Filled.ArrowForward, AppTheme.colors.neutral999) + ) {}, + onClickClose = {} + ) + } +} + +@LightDarkPreview +@Composable +private fun OnlyStartIconBannerPreview() { + PreviewAppTheme { + Banner( + title = "Lorem ipsum dolor sit amet", + startIcon = BannerClickableIcon(BannerIcon.Warning) {}, + contentColor = AppTheme.colors.neutral000, + containerColor = AppTheme.colors.yellow600, + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + + "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et" + ) + } +} + +@LightDarkPreview +@Composable +private fun OnlyGearsIconBannerPreview() { + PreviewAppTheme { + Banner( + title = "Lorem ipsum dolor sit amet", + contentColor = AppTheme.colors.neutral999, + containerColor = AppTheme.colors.primary200, + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + + "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et", + gearsIcon = BannerClickableIcon(BannerIcon.Gears) {} + ) + } +} + +@LightDarkPreview +@Composable +private fun BottomTextBannerPreview() { + PreviewAppTheme { + Banner( + title = "404 Seite?", + contentColor = AppTheme.colors.neutral999, + containerColor = AppTheme.colors.primary100, + text = "Bitte aktivieren Sie in Ihren Einstellungen das Öffnen von Links, um fortzufahren", + bottomIcon = BannerClickableTextIcon( + text = "Lorem ipsum dolor sit amet", + icon = BannerIcon.Custom(Icons.AutoMirrored.Filled.ArrowForward, AppTheme.colors.primary600) + ) {}, + onClickClose = {} + ) + } +} + +@LightDarkPreview +@Composable +private fun NoInternetBannerPreview() { + PreviewAppTheme { + Banner( + contentColor = AppTheme.colors.neutral000, + containerColor = AppTheme.colors.yellow600, + text = "Kein Internet" + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt index 9e2af4af..94fda277 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt index 019217ce..f6aedfde 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt index 7f9c4dca..3269777c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerMedium @Composable fun BottomSheetAction( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt index 99716dab..c6d7b3ef 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -22,18 +22,23 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonElevation +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults // TODO replace with material3 @Composable @@ -42,8 +47,8 @@ fun SecondaryButton( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors( backgroundColor = AppTheme.colors.neutral100, @@ -74,8 +79,8 @@ fun TertiaryButton( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = BorderStroke(width = 1.dp, color = AppTheme.colors.neutral300), colors: ButtonColors = ButtonDefaults.buttonColors( backgroundColor = AppTheme.colors.neutral050, @@ -106,8 +111,8 @@ fun PrimaryButtonLarge( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), content: @Composable RowScope.() -> Unit @@ -133,8 +138,8 @@ fun PrimaryButtonSmall( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), content: @Composable RowScope.() -> Unit @@ -160,8 +165,8 @@ fun PrimaryButtonTiny( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), content: @Composable RowScope.() -> Unit @@ -187,8 +192,8 @@ fun PrimaryButton( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 4.dp), - shape: Shape = RoundedCornerShape(8.dp), + elevation: ButtonElevation? = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = SizeDefaults.half), + shape: Shape = RoundedCornerShape(SizeDefaults.one), border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = PaddingValues( @@ -209,3 +214,38 @@ fun PrimaryButton( contentPadding = contentPadding, content = content ) + +@Composable +fun OutlinedIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String?, + text: String? = null, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + elevation: ButtonElevation? = null, + shape: Shape = RoundedCornerShape(SizeDefaults.one), + border: BorderStroke? = ButtonDefaults.outlinedBorder, + colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding +) { + OutlinedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + elevation = elevation, + shape = shape, + border = border, + colors = colors, + interactionSource = interactionSource, + contentPadding = contentPadding + ) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + tint = AppTheme.colors.primary600, + modifier = Modifier.size(SizeDefaults.triple) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/CameraTopBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/CameraTopBar.kt new file mode 100644 index 00000000..b9be3e4a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/CameraTopBar.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.IconToggleButton +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.FlashOff +import androidx.compose.material.icons.rounded.FlashOn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R + +@Composable +fun CameraTopBar( + flashEnabled: Boolean, + onClickClose: () -> Unit, + onFlashClick: (Boolean) -> Unit +) { + Surface( + color = Color.Unspecified, + contentColor = Color.White, + modifier = Modifier.fillMaxWidth() + ) { + Row(modifier = Modifier.padding(16.dp)) { + val accCancel = stringResource(R.string.cam_acc_cancel) + val accTorch = stringResource(R.string.cam_acc_torch) + + IconButton( + onClick = { onClickClose() }, + modifier = Modifier + .testTag("camera/closeButton") + .semantics { contentDescription = accCancel } + ) { + Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.weight(1f)) + + IconToggleButton( + checked = flashEnabled, + onCheckedChange = onFlashClick, + modifier = Modifier + .testTag("camera/flashToggle") + .semantics { contentDescription = accTorch } + ) { + val ic = if (flashEnabled) { + Icons.Rounded.FlashOn + } else { + Icons.Rounded.FlashOff + } + Icon(ic, null, modifier = Modifier.size(24.dp)) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Center.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Center.kt new file mode 100644 index 00000000..1f6d5e81 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Center.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun Center( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.BottomCenter, + content = content + ) +} + +@Composable +fun CenterColumn( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + content() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt index cc4d5dbf..764325ba 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall @Composable fun Chip( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt index 0ba09bd4..2046f1de 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt index 1c1e8755..ef879818 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("TooManyFunctions", "MagicNumber") + package de.gematik.ti.erp.app.utils.compose import android.content.ActivityNotFoundException @@ -32,7 +34,9 @@ import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -64,21 +68,21 @@ import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.TextFieldColors -import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.Undo import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.material.icons.rounded.Undo +import androidx.compose.material3.TextFieldColors import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -86,13 +90,16 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -121,19 +128,26 @@ import androidx.compose.ui.window.DialogProperties import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.LocalTimeZone import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid +import de.gematik.ti.erp.app.utils.isNotNullOrEmpty import io.github.aakira.napier.Napier import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.format +import kotlinx.datetime.format.Padding import kotlinx.datetime.toLocalDateTime import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Date +import kotlinx.datetime.LocalDateTime as KotlinLocalDateTime @Composable fun LargeButton( @@ -271,17 +285,17 @@ fun ClickableTaggedText( } @Composable -fun NavigationBack(modifier: Modifier = Modifier, onClick: () -> Unit) { - val acc = stringResource(R.string.back) +fun NavigateBackButton(modifier: Modifier = Modifier, onClick: () -> Unit) { + val contentDescription = stringResource(R.string.back) IconButton( onClick = onClick, modifier = modifier - .semantics { contentDescription = acc } + .semantics { this.contentDescription = contentDescription } .testTag(TestTag.TopNavigation.BackButton) ) { Icon( - Icons.Rounded.ArrowBack, + Icons.AutoMirrored.Rounded.ArrowBack, null, tint = MaterialTheme.colors.primary, modifier = Modifier.size(24.dp) @@ -296,20 +310,29 @@ enum class NavigationBarMode { @Composable fun NavigationTopAppBar( + modifier: Modifier = Modifier, navigationMode: NavigationBarMode?, title: String, + isTitleCentered: Boolean = false, backgroundColor: Color = MaterialTheme.colors.surface, elevation: Dp = AppBarDefaults.TopAppBarElevation, actions: @Composable RowScope.() -> Unit = {}, onBack: () -> Unit ) = TopAppBar( + modifier = modifier, title = { - Text(title, overflow = TextOverflow.Ellipsis) + if (isTitleCentered) { + Center { + Text(title, overflow = TextOverflow.Ellipsis) + } + } else { + Text(title, overflow = TextOverflow.Ellipsis) + } }, backgroundColor = backgroundColor, navigationIcon = { when (navigationMode) { - NavigationBarMode.Back -> NavigationBack { onBack() } + NavigationBarMode.Back -> NavigateBackButton { onBack() } NavigationBarMode.Close -> NavigationClose { onBack() } else -> {} } @@ -326,7 +349,7 @@ fun LabeledSwitch( enabled: Boolean = true, icon: ImageVector, header: String, - description: String? + description: String? = null ) { LabeledSwitch( checked = checked, @@ -384,15 +407,15 @@ fun LabeledSwitch( indication = LocalIndication.current ) .fillMaxWidth() - .padding(16.dp) + .padding(SizeDefaults.double) .semantics(true) {}, verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(SizeDefaults.onefold) ) { label() // for better visibility in dark mode - CompositionLocalProvider(LocalAbsoluteElevation provides 8.dp) { + CompositionLocalProvider(LocalAbsoluteElevation provides SizeDefaults.onefold) { Switch( checked = checked, onCheckedChange = null, @@ -402,6 +425,61 @@ fun LabeledSwitch( } } +@Composable +fun LabelButton( + icon: ImageVector, + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Icon(icon, null, tint = AppTheme.colors.primary600) + SpacerMedium() + Text( + modifier = Modifier.weight(1f), + text = text, + style = AppTheme.typography.body1 + ) + Icon(Icons.AutoMirrored.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@Composable +fun LabelButton( + icon: Painter, + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Image(painter = icon, contentDescription = null) + SpacerMedium() + Text( + modifier = Modifier.weight(1f), + text = text, + style = AppTheme.typography.body1 + ) + Icon(Icons.AutoMirrored.Outlined.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } +} + +@Suppress("SpreadOperator") @Composable fun annotatedStringResource(@StringRes id: Int, vararg args: Any): AnnotatedString = annotatedStringResource(id, *(args.map { AnnotatedString(it.toString()) }.toTypedArray())) @@ -467,6 +545,27 @@ fun annotatedStringBold(text: String) = } } +@Composable +fun annotatedLinkUnderlined(fullText: String, clickableText: String, tag: String): AnnotatedString { + val startIndex = fullText.indexOf(clickableText) + val endIndex = startIndex + clickableText.length + + return buildAnnotatedString { + append(fullText) + addStyle( + style = SpanStyle(color = AppTheme.colors.primary600, textDecoration = TextDecoration.Underline), + start = startIndex, + end = endIndex + ) + addStringAnnotation( + tag = tag, + start = startIndex, + end = endIndex, + annotation = clickableText + ) + } +} + @Deprecated( "Please do not use this function anymore. Use ErezeptAlertDialog instead.", replaceWith = ReplaceWith("de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog"), @@ -733,7 +832,7 @@ fun InputField( onValueChange: (String) -> Unit, onSubmit: (value: String) -> Unit, label: @Composable (() -> Unit)? = null, - colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(), + colors: TextFieldColors = erezeptTextFieldColors(), isError: Boolean = false, errorText: @Composable (() -> Unit)? = null, keyBoardType: KeyboardType? = null @@ -741,7 +840,7 @@ fun InputField( val initialValue = rememberSaveable { value } val undoDescription = stringResource(R.string.onb_undo_description) Column { - OutlinedTextField( + ErezeptOutlineText( value = value, onValueChange = { onValueChange(it) @@ -770,7 +869,7 @@ fun InputField( .semantics { contentDescription = undoDescription }, onClick = { onValueChange(initialValue) } ) { - Icon(Icons.Rounded.Undo, null) + Icon(Icons.AutoMirrored.Rounded.Undo, null) } } } else { @@ -809,22 +908,26 @@ fun phrasedDateString(date: LocalDateTime): String { return "${date.format(dateFormatter)} $at ${timeFormatter.format(timeOfDate)}" } -fun dateString(date: kotlinx.datetime.LocalDateTime): String { - return dateString(date.toJavaLocalDateTime()) -} - -fun timeString(time: kotlinx.datetime.LocalDateTime): String { - return timeString(time.toJavaLocalDateTime()) -} - -fun dateString(date: LocalDateTime): String { - val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - return date.format(dateFormatter) +fun dateString(date: KotlinLocalDateTime): String { + return date.format( + KotlinLocalDateTime.Format { + dayOfMonth(Padding.ZERO) + chars(".") + monthNumber(Padding.ZERO) + chars(".") + year() + } + ) } -fun timeString(date: LocalDateTime): String { - val timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - return date.format(timeFormatter) +fun timeString(time: KotlinLocalDateTime): String { + return time.format( + KotlinLocalDateTime.Format { + hour() + chars(":") + minute() + } + ) } /** @@ -832,12 +935,8 @@ fun timeString(date: LocalDateTime): String { */ @Composable fun dateWithIntroductionString(@StringRes id: Int, instant: Instant): String { - val dateFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - val date = remember { - instant.toLocalDateTime(TimeZone.currentSystemDefault()) - .toJavaLocalDateTime() - .toLocalDate().format(dateFormatter) - } + val zone = LocalTimeZone.current + val date = remember { dateString(instant.toLocalDateTime(zone)) } val combinedString = annotatedStringResource(id, date).toString() return remember { combinedString } } @@ -871,6 +970,7 @@ fun Label( label: String? = null, onClick: (() -> Unit)? = null ) { + val haptic = LocalHapticFeedback.current val clipboardManager = LocalClipboardManager.current val context = LocalContext.current @@ -889,7 +989,8 @@ fun Label( onClick?.invoke() }, onLongClick = { - if (text != null) { + if (text.isNotNullOrEmpty() && text != null) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(text)) Toast .makeText(context, "$label $text", Toast.LENGTH_SHORT) @@ -943,10 +1044,22 @@ fun HealthPortalLink( annotatedLink .getStringAnnotations("URL", it, it) .firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) + uriHandler.openUriWhenValid(stringAnnotation.item) } }, modifier = Modifier.align(Alignment.End) ) } } + +@Composable +fun rememberContentPadding(innerPadding: PaddingValues) = remember(innerPadding) { + derivedStateOf { + PaddingValues( + top = PaddingDefaults.Medium, + bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt index 528f4d6c..36195b8b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -21,20 +21,25 @@ package de.gematik.ti.erp.app.utils.compose import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +@Stable class ComposableEvent { - private var trigger by mutableStateOf(false) + private val triggerFlow = MutableStateFlow(null) var payload: T? by mutableStateOf(null) fun trigger(payload: T) { this.payload = payload - trigger = true + triggerFlow.value = payload } @Composable @@ -42,13 +47,26 @@ class ComposableEvent { fun listen( block: suspend CoroutineScope.(payload: T) -> Unit ) { - LaunchedEffect(trigger) { - if (trigger) { - trigger = false + LaunchedEffect(triggerFlow) { + triggerFlow.collectLatest { payload -> + payload?.let { + block(payload) + triggerFlow.value = null + } + } + } + } - // Has to be set via trigger as `T` - @Suppress("UNCHECKED_CAST") - block(payload as T) + fun listen( + coroutineScope: CoroutineScope, + block: suspend CoroutineScope.(payload: T) -> Unit + ) { + coroutineScope.launch { + triggerFlow.collectLatest { payload -> + payload?.let { + block(it) + triggerFlow.value = null + } } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt index f8606c52..e1f67cf1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ConfirmationPasswordTextField.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ConfirmationPasswordTextField.kt index a898be69..19909f41 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ConfirmationPasswordTextField.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ConfirmationPasswordTextField.kt @@ -1,86 +1,66 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose -import androidx.compose.material.ContentAlpha -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable fun ConfirmationPasswordTextField( modifier: Modifier, - password: String, - passwordScore: Int, value: String, + passwordIsValidAndConsistent: Boolean, + repeatedPasswordHasError: Boolean, onValueChange: (String) -> Unit, onSubmit: () -> Unit ) { - val isError = remember(password, value) { - password.isNotBlank() && - value.isNotBlank() && - !password.startsWith(value) - } - - val isConsistent = remember(password, value) { - password.isNotBlank() && password == value && validatePasswordScore(passwordScore) - } - PasswordTextField( modifier = modifier, value = value, onValueChange = onValueChange, - isConsistent = isConsistent, - isError = isError, + isConsistent = passwordIsValidAndConsistent, + isError = repeatedPasswordHasError, onSubmit = { - if (!isError && isConsistent) { + if (!repeatedPasswordHasError && passwordIsValidAndConsistent) { onSubmit() } }, allowAutofill = true, allowVisiblePassword = true, label = { - Text(stringResource(R.string.settings_password_repeat_password)) + Text(stringResource(R.string.settings_password_repeat)) }, - colors = if (isConsistent) { - TextFieldDefaults.outlinedTextFieldColors( - focusedBorderColor = AppTheme.colors.green600.copy( - alpha = ContentAlpha.high - ), - focusedLabelColor = AppTheme.colors.green600.copy( - alpha = ContentAlpha.high - ), - unfocusedBorderColor = AppTheme.colors.green600.copy(alpha = ContentAlpha.high), - unfocusedLabelColor = AppTheme.colors.green600.copy( - alpha = ContentAlpha.high - ), - trailingIconColor = AppTheme.colors.green600.copy( - alpha = ContentAlpha.high - ) + colors = if (passwordIsValidAndConsistent) { + erezeptTextFieldColors( + focussedBorderColor = AppTheme.colors.green600, + focussedLabelColor = AppTheme.colors.green600, + unfocusedBorderColor = AppTheme.colors.green600, + unfocusedLabelColor = AppTheme.colors.green600, + focusedTrailingIconColor = AppTheme.colors.green600 ) } else { - TextFieldDefaults.outlinedTextFieldColors() + erezeptTextFieldColors() } ) } @@ -92,8 +72,8 @@ fun ConfirmationPasswordTextFieldPreview() { ConfirmationPasswordTextField( modifier = Modifier, value = "", - password = "", - passwordScore = 1, + passwordIsValidAndConsistent = false, + repeatedPasswordHasError = false, onSubmit = {}, onValueChange = {} ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt index f0a2e92e..0186c943 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt @@ -1,24 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose import android.graphics.Bitmap +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -35,9 +37,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ZoomIn import androidx.compose.material.icons.rounded.ZoomOut import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,12 +47,13 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp import com.google.zxing.BarcodeFormat import com.google.zxing.common.BitMatrix import com.google.zxing.datamatrix.DataMatrixWriter import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium import kotlin.math.max import kotlin.math.roundToInt @@ -66,24 +69,25 @@ fun DataMatrix( ) { var isZoomedOut by remember { mutableStateOf(false) } + // Animating the scale factor + val scale by animateFloatAsState(targetValue = if (isZoomedOut) ScaleOutValue else ScaleInValue) + AppTheme(darkTheme = false) { - val shape = RoundedCornerShape(16.dp) + val shape = RoundedCornerShape(SizeDefaults.double) Column( modifier = modifier .background(AppTheme.colors.neutral000, shape) - .border(1.dp, AppTheme.colors.neutral300, shape) + .border(SizeDefaults.eighth, AppTheme.colors.neutral300, shape) .padding(PaddingDefaults.Medium) ) { - ( - Box( - modifier = Modifier - .scale(scale = if (isZoomedOut) ScaleOutValue else ScaleInValue) - .drawDataMatrix(matrix) - .aspectRatio(1f) - .fillMaxWidth() - ) - ) + Box( + modifier = Modifier + .scale(scale) + .drawDataMatrix(matrix) + .aspectRatio(1f) + .fillMaxWidth() + ) SpacerMedium() Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { if (codeName != null) { @@ -102,9 +106,11 @@ fun DataMatrix( TertiaryButton( onClick = { isZoomedOut = !isZoomedOut } ) { - when { - isZoomedOut -> Icon(Icons.Rounded.ZoomIn, null) - else -> Icon(Icons.Rounded.ZoomOut, null) + Crossfade(targetState = isZoomedOut) { isZoomedOut -> + when { + isZoomedOut -> Icon(Icons.Rounded.ZoomIn, null) + else -> Icon(Icons.Rounded.ZoomOut, null) + } } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt index 11485ad8..e215a07e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -56,13 +56,15 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.MainAxisAlignment -import androidx.compose.ui.window.Dialog import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium import kotlinx.coroutines.flow.filter + @Deprecated( "Please do not use this function anymore. Use ErezeptAlertDialog instead.", replaceWith = ReplaceWith("de.gematik.ti.erp.app.utils.compose.ErezeptAlertDialog"), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DisableZoomWhileActive.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DisableZoomWhileActive.kt new file mode 100644 index 00000000..e7d95e03 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DisableZoomWhileActive.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import de.gematik.ti.erp.app.base.BaseActivity + +/** + * Disable zoom while the screen using this composable is active + */ +@Suppress("ComposableNaming") +@Composable +fun ComponentActivity.disableZoomWhileActive() { + val activity = this as? BaseActivity + DisposableEffect(Unit) { + activity?.disableZoomTemporarily?.value = true + onDispose { + activity?.disableZoomTemporarily?.value = false + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Divider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Divider.kt new file mode 100644 index 00000000..bd26405f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Divider.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun ElevationDivider() = Divider( + modifier = Modifier.shadow(SizeDefaults.quarter), + thickness = SizeDefaults.sixteenth +) + +@Composable +fun BorderDivider() = Divider(thickness = SizeDefaults.sixteenth) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt index 9d20d743..19d2f05b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text @@ -49,12 +48,13 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.extensions.ErezeptKeyboardOptions const val TextMinLength = 1 const val MaxLines = 3 @@ -140,7 +140,7 @@ fun EditableTextField( modifier = Modifier.focusRequester(focusRequester), colors = basicTextFieldColors, textStyle = AppTheme.typography.h5.copy(textAlign = TextAlign.Center), - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), + keyboardOptions = ErezeptKeyboardOptions.defaultDone, isError = isError, value = name, onValueChange = { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EmptyScreenComponent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EmptyScreenComponent.kt new file mode 100644 index 00000000..e8efe3d0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EmptyScreenComponent.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall + +@Composable +fun EmptyScreenComponent( + modifier: Modifier = Modifier, + title: String, + body: String, + image: @Composable () -> Unit = { + Image( + painterResource(R.drawable.girl_red_oh_no), + contentDescription = null, + modifier = Modifier.size(SizeDefaults.twentyfold) + ) + }, + button: @Composable () -> Unit +) = + Box( + modifier = modifier + .fillMaxSize() + .padding(PaddingDefaults.Medium), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + image() + SpacerMedium() + Text( + text = title, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) + SpacerSmall() + Text( + text = body, + style = AppTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().wrapContentHeight() + ) + SpacerSmall() + button() + } + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/End.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/End.kt new file mode 100644 index 00000000..b5a936a1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/End.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun End( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.TopEnd, + content = content + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptAlertDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptAlertDialog.kt index 01b9fa39..ed1b1568 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptAlertDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptAlertDialog.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("UnusedPrivateMember") + package de.gematik.ti.erp.app.utils.compose import androidx.compose.foundation.BorderStroke @@ -34,14 +36,16 @@ import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.contentColorFor import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.AddTask -import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material3.AlertDialog +import androidx.compose.material.icons.rounded.Coronavirus +import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -50,43 +54,168 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.compose.ErezeptText.ErezeptTextAlignment.Center -import de.gematik.ti.erp.app.utils.compose.ErezeptText.ErezeptTextAlignment.Default +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment.Center +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment.Default +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme import de.gematik.ti.erp.app.utils.letNotNullOnCondition +//region Alert dialog with only title and one button @Composable fun ErezeptAlertDialog( modifier: Modifier = Modifier, title: String, - body: String, okText: String = stringResource(R.string.ok), - titleAlignment: ErezeptText.ErezeptTextAlignment = Center, + dismissTextColor: Color? = null, + titleAlignment: TextAlignment = Center, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, buttonsArrangement: Arrangement.Horizontal = Arrangement.End, dismissButtonIcon: ImageVector? = null, - onDismissRequest: () -> Unit, - onConfirmRequest: () -> Unit + onDismissRequest: () -> Unit ) { InternalErezeptAlertDialog( modifier = modifier, title = title, titleAlignment = titleAlignment, - body = body, + body = null, buttonsArrangement = buttonsArrangement, + dismissButtonTestTag = dismissButtonTestTag, dismissText = okText, + dismissTextColor = dismissTextColor, dismissButtonIcon = dismissButtonIcon, + onConfirmRequest = onDismissRequest, + onDismissRequest = onDismissRequest, + onDismissButtonRequest = onDismissRequest + ) +} + +@LightDarkPreview +@Composable +fun ErezeptAlertDialogWithOnlyTitlePreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with only title and one button", + onDismissRequest = {} + ) + } +} + +//endregion + +//region Alert dialog with only body and one button +@Composable +fun ErezeptAlertDialog( + modifier: Modifier = Modifier, + body: String, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, + confirmButtonTestTag: String = TestTag.AlertDialog.ConfirmButton, + okText: String = stringResource(R.string.ok), + dismissText: String = stringResource(R.string.cancel), + buttonsArrangement: Arrangement.Horizontal = Arrangement.End, + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit +) { + InternalErezeptAlertDialog( + modifier = modifier, + body = body, + buttonsArrangement = buttonsArrangement, + confirmText = okText, + dismissText = dismissText, + confirmButtonTestTag = confirmButtonTestTag, + dismissButtonTestTag = dismissButtonTestTag, onConfirmRequest = onConfirmRequest, - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + onDismissButtonRequest = onDismissRequest ) } +@LightDarkPreview +@Composable +fun ErezeptAlertDialogWithOnlyBodyPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + body = "Dialog with only body and two buttons", + onConfirmRequest = {}, + onDismissRequest = {} + ) + } +} + +//endregion + +//region Alert dialog with one button @Composable fun ErezeptAlertDialog( modifier: Modifier = Modifier, title: String, body: String, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, + okText: String = stringResource(R.string.ok), + titleAlignment: TextAlignment = Center, + buttonsArrangement: Arrangement.Horizontal = Arrangement.End, + dismissButtonIcon: ImageVector? = null, + onDismissRequest: () -> Unit +) { + InternalErezeptAlertDialog( + modifier = modifier, + title = title, + titleAlignment = titleAlignment, + body = body, + buttonsArrangement = buttonsArrangement, + dismissButtonTestTag = dismissButtonTestTag, + dismissText = okText, + dismissButtonIcon = dismissButtonIcon, + onConfirmRequest = onDismissRequest, + onDismissRequest = onDismissRequest, + onDismissButtonRequest = onDismissRequest + ) +} + +@LightDarkPreview +@Composable +fun ErezeptAlertDialogPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with one button", + body = "A dialog is a type of modal window that appears in front of app content to " + + "provide critical information, or ask for decision", + onDismissRequest = {} + ) + } +} + +@LightDarkPreview +@Composable +fun ErezeptAlertDialogWithIconPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with one button and dismiss icon", + titleAlignment = Default, + dismissButtonIcon = Icons.AutoMirrored.Filled.ArrowForward, + body = "A dialog is a type of modal window that appears in front of app content to " + + "provide critical information, or ask for decision", + onDismissRequest = {} + ) + } +} +//endregion + +//region Alert dialog with two buttons +@Composable +fun ErezeptAlertDialog( + modifier: Modifier = Modifier, + title: String, + titleIcon: ImageVector? = null, + bodyText: String, + confirmButtonTestTag: String = TestTag.AlertDialog.ConfirmButton, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, confirmText: String = stringResource(R.string.ok), dismissText: String, - titleAlignment: ErezeptText.ErezeptTextAlignment = Center, + confirmTextColor: Color? = null, + dismissTextColor: Color? = null, + titleAlignment: TextAlignment = Center, buttonsArrangement: Arrangement.Horizontal = Arrangement.End, confirmButtonIcon: ImageVector? = null, dismissButtonIcon: ImageVector? = null, @@ -96,121 +225,175 @@ fun ErezeptAlertDialog( InternalErezeptAlertDialog( modifier = modifier, title = title, - body = body, + titleIcon = titleIcon, + body = bodyText, confirmText = confirmText, dismissText = dismissText, + confirmTextColor = confirmTextColor, + dismissTextColor = dismissTextColor, + dismissButtonTestTag = dismissButtonTestTag, + confirmButtonTestTag = confirmButtonTestTag, confirmButtonIcon = confirmButtonIcon, dismissButtonIcon = dismissButtonIcon, titleAlignment = titleAlignment, buttonsArrangement = buttonsArrangement, onConfirmRequest = onConfirmRequest, - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + onDismissButtonRequest = onDismissRequest ) } -@OptIn(ExperimentalMaterial3Api::class) +// If we need the dismiss button to do something different than the cancel button, use this composable @Composable -private fun InternalErezeptAlertDialog( +fun ErezeptAlertDialog( modifier: Modifier = Modifier, title: String, - titleAlignment: ErezeptText.ErezeptTextAlignment = Center, + titleIcon: ImageVector? = null, body: String, - confirmText: String? = null, + confirmText: String = stringResource(R.string.ok), dismissText: String, + titleAlignment: TextAlignment = Center, buttonsArrangement: Arrangement.Horizontal = Arrangement.End, confirmButtonIcon: ImageVector? = null, dismissButtonIcon: ImageVector? = null, - onConfirmRequest: (() -> Unit)? = null, - onDismissRequest: () -> Unit + onConfirmRequest: () -> Unit, + onDismissRequest: () -> Unit, + onDismissButtonRequest: () -> Unit ) { - AlertDialog( + InternalErezeptAlertDialog( modifier = modifier, - onDismissRequest = onDismissRequest - ) { - Surface( - color = AppTheme.colors.neutral025, - shape = RoundedCornerShape(SizeDefaults.triple), - border = BorderStroke( - width = SizeDefaults.eighth, - color = AppTheme.colors.neutral400 - ), - contentColor = contentColorFor(AppTheme.colors.neutral025) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Large) + title = title, + titleIcon = titleIcon, + body = body, + confirmText = confirmText, + dismissText = dismissText, + confirmButtonIcon = confirmButtonIcon, + dismissButtonIcon = dismissButtonIcon, + titleAlignment = titleAlignment, + buttonsArrangement = buttonsArrangement, + onConfirmRequest = onConfirmRequest, + onDismissRequest = onDismissRequest, + onDismissButtonRequest = onDismissButtonRequest + ) +} + +@LightDarkPreview +@Composable +fun ErezeptAlertTwoButtonsDialogPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with two buttons", + bodyText = "A dialog is a type of modal window that appears in front of app content to " + + "provide critical information, or ask for decision", + confirmText = "Ok", + dismissText = "Cancel", + onDismissRequest = {}, + onConfirmRequest = {} + ) + } +} + +@LightDarkPreview +@Composable +fun ErezeptAlertTwoButtonsWithIconsDialogPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with two buttons and icons", + bodyText = "A dialog is a type of modal window that appears in front of app content to " + + "provide critical information, or ask for decision", + confirmText = "Ok", + dismissText = "Cancel", + confirmButtonIcon = Icons.Default.AddTask, + dismissButtonIcon = Icons.Default.Cancel, + onDismissRequest = {}, + onConfirmRequest = {} + ) + } +} + +//endregion + +//region Alert dialog with custom buttons +@Composable +fun ErezeptAlertDialog( + modifier: Modifier = Modifier, + title: String, + titleIcon: ImageVector? = null, + titleAlignment: TextAlignment = Center, + bodyAlignment: TextAlignment = Default, + body: String, + onDismissRequest: () -> Unit, + buttons: @Composable ColumnScope.() -> Unit +) { + InternalErezeptAlertDialog( + modifier = modifier, + title = title, + titleIcon = titleIcon, + titleAlignment = titleAlignment, + bodyAlignment = bodyAlignment, + body = body, + onDismissRequest = onDismissRequest, + buttons = { + // centering the buttons + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center ) { - SpacerMedium() - ErezeptText.Title( - text = title, - textAlignment = titleAlignment - ) - SpacerMedium() - ErezeptText.Body(body) - SpacerMedium() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = buttonsArrangement - ) { - TextButton( - modifier = Modifier.testTag(TestTag.AlertDialog.CancelButton), - onClick = onDismissRequest - ) { - Text(dismissText) - dismissButtonIcon?.let { dismissIcon -> - SpacerTiny() - Icon( - imageVector = dismissIcon, - contentDescription = null - ) - } - } - letNotNullOnCondition( - first = confirmText, - condition = { onConfirmRequest != null } - ) { text -> - TextButton( - modifier = Modifier.testTag(TestTag.AlertDialog.ConfirmButton), - onClick = { - onConfirmRequest?.invoke() - } - ) { - Text(text) - confirmButtonIcon?.let { confirmIcon -> - SpacerTiny() - Icon( - imageVector = confirmIcon, - contentDescription = null - ) - } - } - } + Column { + buttons() } - SpacerMedium() } } + ) +} + +@LightDarkPreview +@Composable +fun ErezeptAlertMultiButtonsDialogPreview() { + PreviewAppTheme { + ErezeptAlertDialog( + title = "Dialog with composable view for buttons", + body = "A dialog is a type of modal window that appears in front of app content to " + + "provide critical information, or ask for decision", + onDismissRequest = {}, + buttons = { + TextButton(onClick = {}) { + Text("Button 1") + } + TextButton(onClick = {}) { + Text("Button 2") + } + TextButton(onClick = {}) { + Text("Button 3") + } + } + ) } } +//endregion + +//region Alert dialog with composable body and one button + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ErezeptAlertDialog( modifier: Modifier = Modifier, title: String, - body: @Composable ColumnScope.() -> Unit, + body: (@Composable ColumnScope.() -> Unit), + confirmButtonTestTag: String = TestTag.AlertDialog.ConfirmButton, okText: String = stringResource(R.string.ok), onDismissRequest: () -> Unit ) { - AlertDialog( - modifier = modifier, - onDismissRequest = onDismissRequest + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier ) { Surface( color = MaterialTheme.colors.surface, shape = RoundedCornerShape(SizeDefaults.triple), border = BorderStroke( - width = SizeDefaults.eighth, + width = SizeDefaults.sixteenth, color = AppTheme.colors.neutral400 ), contentColor = contentColorFor(MaterialTheme.colors.surface) @@ -226,14 +409,14 @@ fun ErezeptAlertDialog( ErezeptText.Title(title) } SpacerMedium() - body() + body.invoke(this) SpacerMedium() Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton( - modifier = Modifier.testTag(TestTag.AlertDialog.ConfirmButton), + modifier = Modifier.testTag(confirmButtonTestTag), onClick = onDismissRequest ) { Text(okText) @@ -247,13 +430,12 @@ fun ErezeptAlertDialog( @LightDarkPreview @Composable -fun ErezeptAlertDialogPreview() { +private fun ErezeptAlertDialogWithNoComposableBodyPreview() { PreviewAppTheme { ErezeptAlertDialog( - title = "Dialog with one button", - body = "A dialog is a type of modal window that apperars in front of app content to " + - "provide critical information, or ask for decision", - onConfirmRequest = {}, + title = "Alert dialog with no composable body and one button", + body = {}, + okText = stringResource(R.string.ok), onDismissRequest = {} ) } @@ -261,30 +443,96 @@ fun ErezeptAlertDialogPreview() { @LightDarkPreview @Composable -fun ErezeptAlertDialogWithIconPreview() { +fun ErezeptAlertDialogWithComposableBodyPreview() { PreviewAppTheme { ErezeptAlertDialog( - title = "Dialog with one button", - titleAlignment = Default, - dismissButtonIcon = Icons.Default.ArrowForward, - body = "A dialog is a type of modal window that apperars in front of app content to " + - "provide critical information, or ask for decision", - onConfirmRequest = {}, + title = "Alert dialog with composable body and one button", + body = { + Text( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + text = "BeispielText" + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Rounded.Coronavirus, null) + } + }, onDismissRequest = {} ) } } +//endregion + +//region Alert dialog with two buttons and composable body +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ErezeptAlertDialog( + modifier: Modifier = Modifier, + title: String, + body: @Composable ColumnScope.() -> Unit, + confirmButtonTestTag: String = TestTag.AlertDialog.ConfirmButton, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, + cancelText: String = stringResource(R.string.cancel), + okText: String = stringResource(R.string.ok), + onDismissRequest: () -> Unit, + onConfirmRequest: () -> Unit +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier + ) { + Surface( + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(SizeDefaults.triple), + border = BorderStroke( + width = SizeDefaults.eighth, + color = AppTheme.colors.neutral400 + ), + contentColor = contentColorFor(MaterialTheme.colors.surface) + ) { + Column( + modifier = Modifier.padding(horizontal = PaddingDefaults.Large) + ) { + SpacerMedium() + ErezeptText.Title(title) + SpacerMedium() + body() + SpacerMedium() + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton( + modifier = Modifier.testTag(dismissButtonTestTag), + onClick = onDismissRequest + ) { + Text(cancelText) + } + TextButton( + modifier = Modifier.testTag(confirmButtonTestTag), + onClick = onConfirmRequest + ) { + Text(okText) + } + } + SpacerMedium() + } + } + } +} + @LightDarkPreview @Composable -fun ErezeptAlertTwoButtonsDialogPreview() { +private fun ErezeptAlertDialogWithNoComposableBodyTwoButtonsPreview() { PreviewAppTheme { ErezeptAlertDialog( - title = "Dialog with two buttons", - body = "A dialog is a type of modal window that apperars in front of app content to " + - "provide critical information, or ask for decision", - confirmText = "Ok", - dismissText = "Cancel", + title = "Alert dialog with no composable body and two buttons", + body = {}, + okText = stringResource(R.string.ok), onDismissRequest = {}, onConfirmRequest = {} ) @@ -293,18 +541,190 @@ fun ErezeptAlertTwoButtonsDialogPreview() { @LightDarkPreview @Composable -fun ErezeptAlertTwoButtonsWithIconsDialogPreview() { +fun ErezeptAlertDialogWithComposableBodyTwoButtonsPreview() { PreviewAppTheme { ErezeptAlertDialog( - title = "Dialog with two buttons", - body = "A dialog is a type of modal window that apperars in front of app content to " + - "provide critical information, or ask for decision", - confirmText = "Ok", - dismissText = "Cancel", - confirmButtonIcon = Icons.Default.AddTask, - dismissButtonIcon = Icons.Default.Cancel, + title = "Alert dialog with composable body and two buttons", + body = { + Text( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + text = "BeispielText" + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Rounded.Coronavirus, null) + } + }, onDismissRequest = {}, onConfirmRequest = {} ) } } + +//endregion + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InternalErezeptAlertDialog( + modifier: Modifier = Modifier, + title: String, + titleIcon: ImageVector? = null, + titleAlignment: TextAlignment = Center, + bodyAlignment: TextAlignment = Default, + body: String, + onDismissRequest: () -> Unit, + buttons: @Composable ColumnScope.() -> Unit +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier.padding(horizontal = PaddingDefaults.Large) + ) { + Surface( + color = AppTheme.colors.neutral025, + shape = RoundedCornerShape(SizeDefaults.triple), + border = BorderStroke( + width = SizeDefaults.eighth, + color = AppTheme.colors.neutral400 + ), + contentColor = contentColorFor(AppTheme.colors.neutral025) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Large) + ) { + titleIcon?.let { titleIcon -> + SpacerMedium() + Icon( + imageVector = titleIcon, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + SpacerMedium() + ErezeptText.Title( + text = title, + textAlignment = titleAlignment + ) + SpacerMedium() + ErezeptText.Body( + text = body, + textAlignment = bodyAlignment + ) + SpacerMedium() + buttons() + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun InternalErezeptAlertDialog( + modifier: Modifier = Modifier, + title: String? = null, + titleIcon: ImageVector? = null, + titleAlignment: TextAlignment = Center, + confirmButtonTestTag: String = TestTag.AlertDialog.ConfirmButton, + dismissButtonTestTag: String = TestTag.AlertDialog.CancelButton, + body: String? = null, + confirmText: String? = null, + dismissText: String, + confirmTextColor: Color? = null, + dismissTextColor: Color? = null, + buttonsArrangement: Arrangement.Horizontal = Arrangement.End, + confirmButtonIcon: ImageVector? = null, + dismissButtonIcon: ImageVector? = null, + onConfirmRequest: (() -> Unit)? = null, + onDismissRequest: () -> Unit, + onDismissButtonRequest: () -> Unit +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier.padding(horizontal = PaddingDefaults.Large) + ) { + Surface( + color = AppTheme.colors.neutral025, + shape = RoundedCornerShape(SizeDefaults.triple), + border = BorderStroke( + width = SizeDefaults.eighth, + color = AppTheme.colors.neutral400 + ), + contentColor = contentColorFor(AppTheme.colors.neutral025) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Large) + ) { + titleIcon?.let { titleIcon -> + SpacerMedium() + Icon( + imageVector = titleIcon, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + SpacerMedium() + title?.let { nonNullTitle -> + ErezeptText.Title( + text = nonNullTitle, + textAlignment = titleAlignment + ) + SpacerMedium() + } + body?.let { nonNullBody -> + ErezeptText.Body(nonNullBody) + SpacerMedium() + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = buttonsArrangement + ) { + TextButton( + modifier = Modifier.testTag(dismissButtonTestTag), + onClick = onDismissButtonRequest + ) { + Text( + text = dismissText, + color = dismissTextColor ?: AppTheme.colors.primary600 + ) + dismissButtonIcon?.let { dismissIcon -> + SpacerTiny() + Icon( + imageVector = dismissIcon, + contentDescription = null + ) + } + } + letNotNullOnCondition( + first = confirmText, + condition = { onConfirmRequest != null } + ) { text -> + TextButton( + modifier = Modifier.testTag(confirmButtonTestTag), + onClick = { + onConfirmRequest?.invoke() + } + ) { + Text( + text = text, + color = confirmTextColor ?: AppTheme.colors.primary600 + ) + confirmButtonIcon?.let { confirmIcon -> + SpacerTiny() + Icon( + imageVector = confirmIcon, + contentDescription = null + ) + } + } + } + } + SpacerMedium() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptBanner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptBanner.kt deleted file mode 100644 index 9c5cd0df..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptBanner.kt +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils.compose - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForward -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.testTag -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.theme.SizeDefaults -import de.gematik.ti.erp.app.utils.letNotNullOnCondition - -private const val MIN_WEIGHT = 0.1f -private const val MAX_WEIGHT = 0.9f -private const val INFO_WEIGHT = 0.15f - -sealed class BannerIcon( - open val vector: ImageVector?, - open val color: Color? -) { - data object NoIcon : BannerIcon(null, null) - data object Gears : BannerIcon(Icons.Default.Settings, Color.White) - data object Info : BannerIcon(Icons.Default.Info, Color.White) - data object Warning : BannerIcon(Icons.Default.Warning, Color.Black) - data class Custom(override val vector: ImageVector, override val color: Color) : BannerIcon(vector, color) -} - -data class BannerClickableIcon( - val icon: BannerIcon, - val onClick: (() -> Unit)? -) - -data class BannerClickableTextIcon( - val text: String?, - val icon: BannerIcon, - val onClick: (() -> Unit)? -) - -@Composable -fun ErezeptBanner( - title: String, - text: String, - contentColor: Color = Color.White, - containerColor: Color = AppTheme.colors.primary300, - startIcon: BannerClickableIcon = BannerClickableIcon(BannerIcon.NoIcon, null), - gearsIcon: BannerClickableIcon = BannerClickableIcon(BannerIcon.NoIcon, null), - bottomIcon: BannerClickableTextIcon = BannerClickableTextIcon(null, BannerIcon.NoIcon, null), - onClickClose: (() -> Unit)? = null -) { - Card( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(SizeDefaults.double)) - .border( - border = BorderStroke(SizeDefaults.eighth, AppTheme.colors.primary500), - shape = RoundedCornerShape(SizeDefaults.double) - ), - colors = CardDefaults.cardColors( - containerColor = containerColor, - contentColor = contentColor - ), - elevation = CardDefaults.cardElevation(SizeDefaults.half) - ) { - val isNoStartIcon = startIcon.icon == BannerIcon.NoIcon - val isNoGearsIcon = gearsIcon.icon == BannerIcon.NoIcon - val isNoBottomIcon = bottomIcon.icon == BannerIcon.NoIcon - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = PaddingDefaults.Small) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = PaddingDefaults.Small), - horizontalArrangement = Arrangement.Absolute.SpaceBetween - ) { - ErezeptText.SubtitleOne( - modifier = Modifier - .padding(horizontal = PaddingDefaults.MediumPlus) - .weight(MAX_WEIGHT), - color = AppTheme.colors.primary600, - text = title - ) - onClickClose?.let { - IconButton( - modifier = Modifier - .fillMaxWidth() - .weight(MIN_WEIGHT) - .padding( - end = PaddingDefaults.ShortMedium - ) - .size(SizeDefaults.triple), - onClick = it - ) { - Icon(imageVector = Icons.Default.Close, contentDescription = "Close") - } - } - } - Row( - modifier = Modifier - .fillMaxWidth() - .let { - when { - isNoStartIcon -> it.padding(horizontal = PaddingDefaults.Medium) - else -> it.padding(end = PaddingDefaults.Medium) - } - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - letNotNullOnCondition( - first = startIcon.icon.vector, - second = startIcon.icon.color, - condition = { !isNoStartIcon && startIcon.onClick != null } - ) { vector, color -> - IconButton( - modifier = Modifier.weight(INFO_WEIGHT), - onClick = { startIcon.onClick?.invoke() } - ) { - Icon( - imageVector = vector, - tint = color, - contentDescription = "Info" - ) - } - } - Text( - modifier = Modifier.weight(MAX_WEIGHT), - style = AppTheme.typography.body1, - text = text - ) - letNotNullOnCondition( - first = gearsIcon.icon.vector, - second = gearsIcon.icon.color, - condition = { !isNoGearsIcon && gearsIcon.onClick != null } - ) { vector, color -> - IconButton( - modifier = Modifier - .weight(MIN_WEIGHT) - .size(SizeDefaults.triple), - onClick = { gearsIcon.onClick?.invoke() } - ) { - Icon( - imageVector = vector, - tint = color, - contentDescription = "Close" - ) - } - } - } - letNotNullOnCondition( - first = bottomIcon.text, - second = bottomIcon.icon.vector, - third = bottomIcon.icon.color, - condition = { !isNoBottomIcon && bottomIcon.onClick != null } - ) { text, vector, color -> - TextButton( - modifier = Modifier - .testTag(TestTag.AlertDialog.ConfirmButton) - .padding(horizontal = PaddingDefaults.Small), - onClick = { bottomIcon.onClick?.invoke() } - ) { - ErezeptText.Body(text = text, color = color) - SpacerTiny() - Icon( - imageVector = vector, - tint = color, - contentDescription = "ArrowForward" - ) - } - } - } - } -} - -@LightDarkPreview -@Composable -fun AllIconsBannerPreview() { - PreviewAppTheme { - ErezeptBanner( - title = "Lorem ipsum dolor sit amet", - text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + - "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et", - startIcon = BannerClickableIcon(BannerIcon.Info) {}, - gearsIcon = BannerClickableIcon(BannerIcon.Gears) {}, - bottomIcon = BannerClickableTextIcon( - text = "Lorem ipsum dolor sit amet", - icon = BannerIcon.Custom(Icons.Default.ArrowForward, AppTheme.colors.neutral999) - ) {}, - onClickClose = {} - ) - } -} - -@LightDarkPreview -@Composable -fun OnlyStartIconBannerPreview() { - PreviewAppTheme { - ErezeptBanner( - title = "Lorem ipsum dolor sit amet", - startIcon = BannerClickableIcon(BannerIcon.Warning) {}, - contentColor = AppTheme.colors.neutral000, - containerColor = AppTheme.colors.yellow600, - text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + - "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et" - ) - } -} - -@LightDarkPreview -@Composable -fun OnlyGearsIconBannerPreview() { - PreviewAppTheme { - ErezeptBanner( - title = "Lorem ipsum dolor sit amet", - contentColor = AppTheme.colors.neutral999, - containerColor = AppTheme.colors.primary200, - text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt " + - "ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et", - gearsIcon = BannerClickableIcon(BannerIcon.Gears) {} - ) - } -} - -@LightDarkPreview -@Composable -fun BottomTextBannerPreview() { - PreviewAppTheme { - ErezeptBanner( - title = "404 Seite?", - contentColor = AppTheme.colors.neutral999, - containerColor = AppTheme.colors.primary100, - text = "Bitte aktivieren Sie in Ihren Einstellungen das Öffnen von Links, um fortzufahren", - bottomIcon = BannerClickableTextIcon( - text = "Lorem ipsum dolor sit amet", - icon = BannerIcon.Custom(Icons.Default.ArrowForward, AppTheme.colors.primary600) - ) {}, - onClickClose = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptOutlineText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptOutlineText.kt new file mode 100644 index 00000000..6912255c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptOutlineText.kt @@ -0,0 +1,386 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Coronavirus +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +/** + * OutlinedTextField with eRezept design + */ +@Composable +fun ErezeptOutlineText( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + singleLine: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + minLines: Int = 1, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + shape: Shape = OutlinedTextFieldDefaults.shape, + colors: TextFieldColors = erezeptTextFieldColors(), + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null +) { + OutlinedTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + readOnly = readOnly, + isError = isError, + label = label, + placeholder = placeholder, + interactionSource = interactionSource, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + maxLines = maxLines, + minLines = minLines, + shape = shape, + colors = colors, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + trailingIcon = trailingIcon, + leadingIcon = leadingIcon + ) +} + +@Composable +fun ErezeptOutlineText( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + singleLine: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + minLines: Int = 1, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + shape: Shape = OutlinedTextFieldDefaults.shape, + colors: TextFieldColors = erezeptTextFieldColors(), + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null +) { + OutlinedTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + readOnly = readOnly, + isError = isError, + label = label, + placeholder = placeholder, + interactionSource = interactionSource, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + maxLines = maxLines, + minLines = minLines, + shape = shape, + colors = colors, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon + ) +} + +@Composable +fun ErezeptOutlineText( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + label: String? = null, + placeholder: String? = null, + singleLine: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + minLines: Int = 1, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + shape: Shape = OutlinedTextFieldDefaults.shape, + colors: TextFieldColors = erezeptTextFieldColors(), + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null +) { + OutlinedTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + readOnly = readOnly, + isError = isError, + label = { label?.let { Text(it) } }, + placeholder = { placeholder?.let { Text(it) } }, + interactionSource = interactionSource, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + maxLines = maxLines, + minLines = minLines, + shape = shape, + colors = colors, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + trailingIcon = trailingIcon, + leadingIcon = leadingIcon + ) +} + +@Composable +fun ErezeptOutlineText( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + label: String? = null, + placeholder: String? = null, + singleLine: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + isError: Boolean = false, + minLines: Int = 1, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + shape: Shape = OutlinedTextFieldDefaults.shape, + colors: TextFieldColors = erezeptTextFieldColors(), + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null +) { + OutlinedTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + readOnly = readOnly, + isError = isError, + label = { label?.let { Text(it) } }, + placeholder = { placeholder?.let { Text(it) } }, + interactionSource = interactionSource, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + maxLines = maxLines, + minLines = minLines, + shape = shape, + colors = colors, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + trailingIcon = trailingIcon, + leadingIcon = leadingIcon + ) +} + +@Composable +fun erezeptButtonColors( + containerColor: Color = AppTheme.colors.primary500, + contentColor: Color = AppTheme.colors.neutral000, + disabledContainerColor: Color = AppTheme.colors.neutral500, + disabledContentColor: Color = AppTheme.colors.neutral000 +) = ButtonDefaults.outlinedButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor +) + +@Composable +fun erezeptTextFieldColors( + textColor: Color = AppTheme.colors.neutral900, + focusedTrailingIconColor: Color = AppTheme.colors.neutral400, + focussedLabelColor: Color = AppTheme.colors.primary400, + unfocusedLabelColor: Color = AppTheme.colors.neutral400, + focussedBorderColor: Color = AppTheme.colors.primary400, + unfocusedBorderColor: Color = AppTheme.colors.neutral400, + disabledBorderColor: Color = AppTheme.colors.primary100, + errorBorderColor: Color = AppTheme.colors.red400, + focusedPlaceholderColor: Color = AppTheme.colors.neutral600, + unfocusedPlaceholderColor: Color = AppTheme.colors.neutral400, + focusedContainerColor: Color = AppTheme.colors.neutral000, + unfocusedContainerColor: Color = AppTheme.colors.neutral000, + disabledContainerColor: Color = AppTheme.colors.neutral000, + errorContainerColor: Color = AppTheme.colors.neutral000, + focusedLeadingIconColor: Color = AppTheme.colors.neutral600, + unfocusedLeadingIconColor: Color = AppTheme.colors.neutral400, + disabledLeadingIconColor: Color = AppTheme.colors.neutral300, + errorLeadingIconColor: Color = AppTheme.colors.red400 +) = + TextFieldColors( + focusedTextColor = textColor, + unfocusedTextColor = AppTheme.colors.neutral600, + disabledTextColor = AppTheme.colors.neutral300, + errorTextColor = AppTheme.colors.red400, + focusedContainerColor = focusedContainerColor, + unfocusedContainerColor = unfocusedContainerColor, + disabledContainerColor = disabledContainerColor, + errorContainerColor = errorContainerColor, + cursorColor = AppTheme.colors.neutral400, + errorCursorColor = AppTheme.colors.red400, + textSelectionColors = LocalTextSelectionColors.current, + focusedIndicatorColor = focussedBorderColor, + unfocusedIndicatorColor = unfocusedBorderColor, + disabledIndicatorColor = disabledBorderColor, + errorIndicatorColor = errorBorderColor, + focusedLeadingIconColor = focusedLeadingIconColor, + unfocusedLeadingIconColor = unfocusedLeadingIconColor, + disabledLeadingIconColor = disabledLeadingIconColor, + errorLeadingIconColor = errorLeadingIconColor, + focusedTrailingIconColor = focusedTrailingIconColor, + unfocusedTrailingIconColor = AppTheme.colors.neutral400, + disabledTrailingIconColor = AppTheme.colors.neutral300, + errorTrailingIconColor = AppTheme.colors.red400, + focusedLabelColor = focussedLabelColor, + unfocusedLabelColor = unfocusedLabelColor, + disabledLabelColor = AppTheme.colors.neutral300, + errorLabelColor = AppTheme.colors.red400, + focusedPlaceholderColor = focusedPlaceholderColor, + unfocusedPlaceholderColor = unfocusedPlaceholderColor, + disabledPlaceholderColor = AppTheme.colors.neutral100, + errorPlaceholderColor = AppTheme.colors.red400, + focusedSupportingTextColor = AppTheme.colors.neutral600, + unfocusedSupportingTextColor = AppTheme.colors.neutral400, + disabledSupportingTextColor = AppTheme.colors.neutral100, + errorSupportingTextColor = AppTheme.colors.red400, + focusedPrefixColor = AppTheme.colors.neutral600, + unfocusedPrefixColor = AppTheme.colors.neutral400, + disabledPrefixColor = AppTheme.colors.neutral100, + errorPrefixColor = AppTheme.colors.red400, + focusedSuffixColor = AppTheme.colors.neutral600, + unfocusedSuffixColor = AppTheme.colors.neutral400, + disabledSuffixColor = AppTheme.colors.neutral100, + errorSuffixColor = AppTheme.colors.red400 + ) + +@LightDarkPreview +@Composable +fun PreviewErezeptOutlineText() { + PreviewAppTheme { + Column(Modifier.padding(PaddingDefaults.Medium)) { + ErezeptOutlineText( + value = "Value", + onValueChange = {}, + label = "Label Text", + placeholder = "Placeholder", + singleLine = true, + readOnly = false, + isError = false, + minLines = 1, + maxLines = 1, + colors = erezeptTextFieldColors() + ) + SpacerMedium() + ErezeptOutlineText( + value = "Value", + onValueChange = {}, + label = "Label Text", + placeholder = "Placeholder", + singleLine = true, + readOnly = false, + isError = true, + minLines = 1, + maxLines = 1, + colors = erezeptTextFieldColors() + ) + SpacerMedium() + ErezeptOutlineText( + value = "Value", + onValueChange = {}, + label = "Label Text", + placeholder = "Placeholder", + singleLine = true, + readOnly = false, + isError = false, + minLines = 1, + maxLines = 1, + trailingIcon = { + Box(Modifier.padding(SizeDefaults.one)) { + Icon(Icons.Rounded.Coronavirus, null) + } + }, + colors = erezeptTextFieldColors() + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptText.kt index 50551f74..a0031852 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErezeptText.kt @@ -1,52 +1,86 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.utils.compose.ErezeptText.ErezeptTextAlignment.Center -import de.gematik.ti.erp.app.utils.compose.ErezeptText.ErezeptTextAlignment.Default +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment.Center +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment.Default +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme object ErezeptText { - enum class ErezeptTextAlignment { + enum class TextAlignment { Center, Default } + sealed interface HeaderStyle { + data object H1 : HeaderStyle + data object H2 : HeaderStyle + data object H3 : HeaderStyle + data object H4 : HeaderStyle + data object H5 : HeaderStyle + data object H6 : HeaderStyle + + @Composable + fun textStyle(): TextStyle = when (this) { + is H1 -> AppTheme.typography.h1 + is H2 -> AppTheme.typography.h2 + is H3 -> AppTheme.typography.h3 + is H4 -> AppTheme.typography.h4 + is H5 -> AppTheme.typography.h5 + is H6 -> AppTheme.typography.h6 + } + } + + enum class BodyStyle { + Body1, Body2, Body1l, Body2l; + + companion object { + @Composable + fun select(style: BodyStyle): TextStyle = when (style) { + Body1 -> AppTheme.typography.body1 + Body2 -> AppTheme.typography.body2 + Body1l -> AppTheme.typography.body1l + Body2l -> AppTheme.typography.body2l + } + } + } + @Composable internal fun Title( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, - textAlignment: ErezeptTextAlignment = Default + style: HeaderStyle = HeaderStyle.H6, + textAlignment: TextAlignment = Default ) { when (textAlignment) { - Center -> CenteredTitle(text, modifier, color) - Default -> UnCenteredTitle(text, modifier, color) + Center -> CenteredTitle(text, modifier, color, style.textStyle()) + Default -> UnCenteredTitle(text, modifier, color, style.textStyle()) } } @@ -54,13 +88,14 @@ object ErezeptText { private fun UnCenteredTitle( text: String, modifier: Modifier = Modifier, - color: Color = Color.Unspecified + color: Color = Color.Unspecified, + style: TextStyle ) { Text( modifier = modifier, text = text, color = color, - style = AppTheme.typography.h6 + style = style ) } @@ -68,13 +103,16 @@ object ErezeptText { private fun CenteredTitle( text: String, modifier: Modifier = Modifier, - color: Color = Color.Unspecified + color: Color = Color.Unspecified, + style: TextStyle ) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Title(text, modifier, color) + Center { + Text( + modifier = modifier, + text = text, + color = color, + style = style + ) } } @@ -112,17 +150,56 @@ object ErezeptText { modifier: Modifier = Modifier, color: Color = Color.Unspecified, maxLines: Int = Int.MAX_VALUE, - overflow: TextOverflow = TextOverflow.Ellipsis + overflow: TextOverflow = TextOverflow.Ellipsis, + textAlignment: TextAlignment = Default, + style: BodyStyle = BodyStyle.Body2 + ) { + when (textAlignment) { + Center -> CenteredBody(text, modifier, color, maxLines, overflow, style) + Default -> UnCenteredBody(text, modifier, color, maxLines, overflow, style) + } + } + + @Composable + private fun UnCenteredBody( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Ellipsis, + style: BodyStyle = BodyStyle.Body2 ) { Text( modifier = modifier, text = text, color = color, maxLines = maxLines, - style = AppTheme.typography.body2, + style = BodyStyle.select(style), overflow = overflow ) } + + @Composable + private fun CenteredBody( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Ellipsis, + style: BodyStyle = BodyStyle.Body2 + ) { + Center { + Text( + modifier = modifier, + text = text, + color = color, + maxLines = maxLines, + style = BodyStyle.select(style), + overflow = overflow, + textAlign = TextAlign.Center + ) + } + } } @LightDarkPreview diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorScreenComponent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorScreenComponent.kt new file mode 100644 index 00000000..c4ab0b76 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorScreenComponent.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun ErrorScreenComponent( + modifier: Modifier = Modifier, + noMaxSize: Boolean = false, + title: String = stringResource(R.string.generic_error_title), + body: String = stringResource(R.string.generic_error_info), + onClickRetry: (() -> Unit)? = null +) = + Box( + modifier = modifier + .then( + if (noMaxSize) Modifier else Modifier.fillMaxSize() + ) + .padding(PaddingDefaults.Medium), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + Text( + text = title, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + Text( + text = body, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + // show retry button if onClickRetry is not null + onClickRetry?.let { + TextButton( + onClick = onClickRetry + ) { + Icon(Icons.Rounded.Refresh, null) + SpacerSmall() + Text(stringResource(R.string.cdw_fasttrack_try_again)) + } + } ?: run { + SpacerMedium() + } + } + } + +@Composable +@LightDarkPreview +fun ErrorScreenComponentPreview() { + PreviewAppTheme { + ErrorScreenComponent( + onClickRetry = {} + ) + } +} + +@Composable +@LightDarkPreview +fun ErrorScreenComponentPreviewNoRetry() { + PreviewAppTheme { + ErrorScreenComponent() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt index c669e5ae..16a58e98 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ExceptionDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ExceptionDialog.kt new file mode 100644 index 00000000..55229818 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ExceptionDialog.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import android.app.Dialog +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import de.gematik.ti.erp.app.base.ClipBoardCopy +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.ErezeptText.TextAlignment +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.BuildConfigExtension +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.capitalizeFirstChar + +@Suppress("FunctionNaming") +fun ExceptionDialog( + error: Throwable, + dialogScaffold: DialogScaffold +) { + if (BuildConfigExtension.isInternalDebug) { + dialogScaffold.show { + val scrollState = rememberScrollState() + ExceptionDialogContent(scrollState, it, error) + } + } +} + +@Suppress("MagicNumber") +@Composable +private fun ExceptionDialogContent( + scrollState: ScrollState, + dialog: Dialog, + error: Throwable +) { + val context = LocalContext.current + Scaffold( + topBar = { + Row { + ErezeptText.Title( + modifier = Modifier.weight(.2f) + .padding(PaddingDefaults.Medium), + textAlignment = TextAlignment.Center, + text = "${error.message?.capitalizeFirstChar()}" + ) + IconButton( + modifier = Modifier.weight(.8f), + onClick = { dialog.dismiss() } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null + ) + } + } + }, + bottomBar = { + Center( + modifier = Modifier.padding(bottom = PaddingDefaults.Medium) + ) { + PrimaryButton( + onClick = { + ClipBoardCopy.copyToClipboard( + context = context, + text = error.stackTraceToString() + ) + dialog.dismiss() + } + ) { + Text(text = "Copy to clipboard") + } + } + } + ) { + Column( + modifier = Modifier + .padding(it) + .padding(PaddingDefaults.Medium) + .verticalScroll(scrollState) + ) { + ErezeptText.Body( + text = error.stackTraceToString(), + color = Color.Red + ) + SpacerMedium() + } + } +} + +@LightDarkPreview +@Composable +internal fun ExceptionDialogPreview() { + PreviewAppTheme { + ExceptionDialogContent( + scrollState = rememberScrollState(), + dialog = Dialog(LocalContext.current), + error = IllegalStateException("This is a test exception") + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/FlatButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/FlatButton.kt new file mode 100644 index 00000000..1f05c5fb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/FlatButton.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun FlatButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, + content: @Composable () -> Unit +) = + Surface( + modifier = modifier.fillMaxWidth(), + onClick = onClick, + shape = RoundedCornerShape(16.dp), + color = AppTheme.colors.neutral025, + border = BorderStroke(1.dp, AppTheme.colors.neutral300), + elevation = 0.dp + ) { + Box(Modifier.padding(PaddingDefaults.Medium)) { + content() + } + } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt index c6297804..3556dbe4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.utils.compose import androidx.compose.animation.AnimatedVisibility @@ -84,6 +86,8 @@ import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.extensions.openUriWhenValid import kotlinx.coroutines.delay import java.util.Locale @@ -383,7 +387,7 @@ fun HintTextLearnMoreButton( HintTextActionButton( modifier = modifier, - onClick = { uriHandler.openUri(uri) }, + onClick = { uriHandler.openUriWhenValid(uri) }, enabled = true, align = align, text = stringResource(R.string.cdw_health_insurance_caption_recognize_healthcard) @@ -394,6 +398,7 @@ fun HintTextLearnMoreButton( @Composable fun HintCloseButton( innerPadding: PaddingValues, + tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit ) { IconButton( @@ -401,7 +406,7 @@ fun HintCloseButton( .padding(top = 2.dp, end = 2.dp), onClick = onClick ) { - Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colors.primary) + Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp), tint = tint) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt index c26bc658..9fa4698a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -21,6 +21,9 @@ package de.gematik.ti.erp.app.utils.compose import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.AppBarDefaults import androidx.compose.material.BottomNavigationDefaults import androidx.compose.material.MaterialTheme @@ -29,20 +32,11 @@ import androidx.compose.material.contentColorFor import androidx.compose.material.primarySurface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding @Composable fun TopAppBar( @@ -62,7 +56,7 @@ fun TopAppBar( ) { androidx.compose.material.TopAppBar( title, - Modifier.statusBarsPadding(), + Modifier.systemBarsPadding(), navigationIcon, actions, backgroundColor, @@ -158,13 +152,3 @@ fun BottomNavigation( } } } - -fun Modifier.minimalSystemBarsPadding() = composed { - val navBarInsetsPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues() - - if (navBarInsetsPadding.calculateBottomPadding() <= 16.dp) { - statusBarsPadding() - } else { - systemBarsPadding() - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ItemsMenu.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ItemsMenu.kt new file mode 100644 index 00000000..b12d132b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ItemsMenu.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.DpOffset +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +@Composable +fun ItemsMenu( + modifier: Modifier = Modifier, + menuItems: List, + onClickItem: (T) -> Unit, + dropDownMenuItem: @Composable (T) -> Unit +) { + val haptic = LocalHapticFeedback.current + var isContextMenuVisible by rememberSaveable { mutableStateOf(false) } + var pressOffset by remember { mutableStateOf(DpOffset.Zero) } + var itemHeight by remember { mutableStateOf(SizeDefaults.zero) } + val density = LocalDensity.current + + Card( + modifier = modifier.onSizeChanged { + itemHeight = with(density) { it.height.toDp() } + }, + elevation = CardDefaults.cardElevation( + defaultElevation = SizeDefaults.half + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .pointerInput(true) { + detectTapGestures( + onTap = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + isContextMenuVisible = true + pressOffset = DpOffset(it.x.toDp(), it.y.toDp()) + }, + onLongPress = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + isContextMenuVisible = true + pressOffset = DpOffset(it.x.toDp(), it.y.toDp()) + } + ) + } + .padding(SizeDefaults.double) + ) { + Text(text = "Open Menu") + DropdownMenu( + expanded = isContextMenuVisible, + onDismissRequest = { isContextMenuVisible = false } + ) { + menuItems.forEach { + DropdownMenuItem( + text = { dropDownMenuItem.invoke(it) }, + onClick = { + onClickItem(it) + isContextMenuVisible = false + } + ) + } + } + } + } +} + +@LightDarkPreview +@Composable +fun ItemsMenuPreview() { + PreviewAppTheme { + ItemsMenu( + menuItems = listOf("Item 1", "Item 2", "Item 3"), + onClickItem = {}, + dropDownMenuItem = { + Box( + modifier = Modifier.width(SizeDefaults.triple) + ) { + Text(text = it) + } + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt index 5a0cbe61..484bc6c1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -21,12 +21,40 @@ package de.gematik.ti.erp.app.utils.compose import android.content.res.Configuration import androidx.compose.ui.tooling.preview.Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + group = "light", + showBackground = true +) internal annotation class LightPreview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + group = "dark", + showBackground = true +) internal annotation class DarkPreview +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + group = "dark", + showBackground = true, + fontScale = 2f +) +internal annotation class BigFontDarkPreview + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + group = "light", + showBackground = true, + fontScale = 2f +) +internal annotation class BigFontLightPreview + +@BigFontLightPreview +@BigFontDarkPreview +internal annotation class BigFontPreview + @LightPreview @DarkPreview internal annotation class LightDarkPreview diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LoadingDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LoadingDialog.kt new file mode 100644 index 00000000..820f8181 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LoadingDialog.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import android.app.Dialog +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoadingDialog( + event: ComposableEvent, + dialogScaffold: DialogScaffold, + onDialogRendered: @Composable (Dialog) -> Unit, + onDismissRequest: () -> Unit +) { + event.listen { + dialogScaffold.show { dialog -> + onDialogRendered(dialog) + AlertDialog( + modifier = Modifier.size(SizeDefaults.fivefold), + onDismissRequest = { + onDismissRequest() + } + ) { + CircularProgressIndicator( + modifier = Modifier.size(SizeDefaults.doubleHalf), + color = AppTheme.colors.primary400 + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoadingDialog( + onDismissRequest: () -> Unit +) { + AlertDialog( + modifier = Modifier.size(SizeDefaults.fivefold), + onDismissRequest = onDismissRequest + ) { + CircularProgressIndicator( + modifier = Modifier.size(SizeDefaults.doubleHalf), + color = AppTheme.colors.primary400 + ) + } +} + +@LightDarkPreview +@Composable +fun LoadingDialogPreview() { + PreviewAppTheme { + LoadingDialog { + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LogoLoadingState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LogoLoadingState.kt new file mode 100644 index 00000000..db89f546 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LogoLoadingState.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.SizeDefaults + +private const val ANIMATION_TIME = 1000 + +@Composable +fun LogoLoadingState() { + val infiniteTransition = rememberInfiniteTransition(label = "InfiniteTransition") + val angle by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(ANIMATION_TIME, easing = LinearEasing) + ), + label = "FloatAnimation" + ) + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier + .align(Alignment.Center) + .size(SizeDefaults.fivefold) + .graphicsLayer { rotationZ = angle }, + painter = painterResource(R.drawable.erp_logo), + contentDescription = "loading-state" + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/OutlinedElevatedCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/OutlinedElevatedCard.kt new file mode 100644 index 00000000..f8655c95 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/OutlinedElevatedCard.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun OutlinedElevatedCard(content: @Composable () -> Unit) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = AppTheme.colors.neutral000), + border = BorderStroke(1.dp, AppTheme.colors.neutral100), + shape = RoundedCornerShape(SizeDefaults.doubleHalf), + elevation = CardDefaults.cardElevation( + defaultElevation = 2.dp + ) + ) { + content() + } +} + +@LightDarkPreview +@Composable +fun OutlinedElevatedCardPreview() { + AppTheme { + OutlinedElevatedCard { + Text("OutlinedElevatedCard", style = AppTheme.typography.body1) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordEvaluation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordEvaluation.kt deleted file mode 100644 index b779bba6..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordEvaluation.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils.compose - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription -import androidx.compose.ui.unit.dp -import com.nulabinc.zxcvbn.StandardDictionaries -import com.nulabinc.zxcvbn.Strength -import com.nulabinc.zxcvbn.ZxcvbnBuilder -import com.nulabinc.zxcvbn.io.Resource -import com.nulabinc.zxcvbn.matchers.DictionaryLoader -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.theme.AppTheme - -private const val TEN_PERCENT = 0.1f -private const val THIRTY_PERCENT = 0.3f -private const val SIXTY_PERCENT = 0.6f -private const val ONE_HUNDRED_PERCENT = 1.0f -private const val FIVE_PERCENT = 0.05f -private const val MINIMAL_PASSWORD_SCORE = 2 - -private enum class PasswordEvaluation { - VeryWeak, Weak, Strong, VeryStrong -} - -@Requirement( - "O.Pass_1", - "O.Pass_2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "To determine the strength of the password, we use Zxcvbn with an additional german dictionary" + - "The strength of the password is shown with bars and colors. " + - "The minimum acceptable value of the score must be > 2." -) -@Composable -fun PasswordStrength( - modifier: Modifier, - password: String, - onScoreChange: (Int) -> Unit -) { - val context = LocalContext.current - val assetManager = context.assets - val germanDictionaryFile = assetManager.open("german_dictionary.txt") - val germanDictionaryRessource = Resource { germanDictionaryFile } - - val passwordStrengthEvaluator = ZxcvbnBuilder().dictionaries( - StandardDictionaries.loadAllDictionaries() - ) - .dictionary(DictionaryLoader("german_dictionary", germanDictionaryRessource).load()) - .build() - - val strength = remember(password) { passwordStrengthEvaluator.measure(password) } - val barLength by animateFloatAsState( - when (strength.score) { - PasswordEvaluation.VeryWeak.ordinal -> TEN_PERCENT - PasswordEvaluation.Weak.ordinal -> THIRTY_PERCENT - PasswordEvaluation.Strong.ordinal -> SIXTY_PERCENT - PasswordEvaluation.VeryStrong.ordinal -> ONE_HUNDRED_PERCENT - else -> FIVE_PERCENT - }, - label = "" - ) - val barColor by animateColorAsState( - when (strength.score) { - PasswordEvaluation.VeryWeak.ordinal -> AppTheme.colors.red400 - PasswordEvaluation.Weak.ordinal -> AppTheme.colors.red300 - PasswordEvaluation.Strong.ordinal -> AppTheme.colors.yellow500 - PasswordEvaluation.VeryStrong.ordinal -> AppTheme.colors.green500 - else -> AppTheme.colors.red500 - }, - label = "" - ) - - DisposableEffect(strength) { - onScoreChange(strength.score) - onDispose { } - } - - Column( - modifier = modifier - .semantics(true) { - stateDescription = if (validatePasswordScore(strength.score)) "sufficient" else "insufficient" - } - ) { - Suggestion(strength) - PasswordStrengthIndicator(barLength, barColor) - PasswordStrengthDescription(strength) - } -} - -@Composable -fun Suggestion(strength: Strength) { - val suggestions = strength.feedback.suggestions.joinToString("\n").trim() - if (suggestions.isNotEmpty()) { - Text( - text = annotatedStringResource( - R.string.settings_password_suggestions, - suggestions - ), - style = AppTheme.typography.caption1l - ) - } -} - -@Composable -fun PasswordStrengthIndicator(barLength: Float, barColor: Color) { - SpacerMedium() - Box( - modifier = Modifier - .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(6.dp)) - .fillMaxWidth() - ) { - Spacer( - modifier = Modifier - .background(color = barColor, shape = RoundedCornerShape(6.dp)) - .fillMaxWidth(barLength) - .height(6.dp) - ) - } - SpacerTiny() -} - -@Composable -fun PasswordStrengthDescription(strength: Strength) { - Row(verticalAlignment = Alignment.CenterVertically) { - when { - strength.score == PasswordEvaluation.VeryStrong.ordinal -> { - Text( - stringResource(R.string.settings_password_strength_very_good), - style = AppTheme.typography.body2l - ) - SpacerTiny() - Icon(Icons.Rounded.Check, null, tint = AppTheme.colors.green600) - } - strength.score > MINIMAL_PASSWORD_SCORE -> { - Text( - stringResource(R.string.settings_password_strength_sufficient), - style = AppTheme.typography.body2l - ) - SpacerTiny() - Icon(Icons.Rounded.Check, null, tint = AppTheme.colors.green600) - } - else -> { - Text( - stringResource(R.string.settings_password_strength_not_sufficient), - style = AppTheme.typography.body2l - ) - SpacerTiny() - Icon(Icons.Rounded.Close, null, tint = AppTheme.colors.red600) - } - } - } -} - -fun validatePasswordScore(score: Int): Boolean = - score > MINIMAL_PASSWORD_SCORE - -@LightDarkPreview -@Composable -fun PasswordStrengthPreview() { - PreviewAppTheme { - PasswordStrength( - Modifier, - "", - onScoreChange = {} - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordScore.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordScore.kt new file mode 100644 index 00000000..c2f599a1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordScore.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerTiny +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme + +private const val TEN_PERCENT = 0.1f +private const val THIRTY_PERCENT = 0.3f +private const val SIXTY_PERCENT = 0.6f +private const val ONE_HUNDRED_PERCENT = 1.0f +private const val FIVE_PERCENT = 0.05f + +enum class PasswordScore { + Uninitialised, VeryWeak, Weak, Strong, VeryStrong +} + +data class PasswordEvaluation( + val score: PasswordScore, + val feedback: String +) { + val isStrongEnough = score >= PasswordScore.Strong +} + +@Requirement( + "O.Pass_1#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Implementation of password strength validation using Zxcvbn with an additional german dictionary" +) +@Requirement( + "O.Pass_2#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Shows password strength with a colored bar and a description" +) +@Composable +fun PasswordStrength( + modifier: Modifier, + passwordEvaluation: PasswordEvaluation +) { + val sufficientText = stringResource(id = R.string.settings_password_sufficient) + val notSufficientText = stringResource(id = R.string.settings_password_not_sufficient) + Column( + modifier = modifier + .semantics(true) { + stateDescription = if (passwordEvaluation.isStrongEnough) sufficientText else notSufficientText + } + ) { + Suggestion(passwordEvaluation.feedback) + PasswordStrengthIndicator(passwordEvaluation.score) + PasswordStrengthDescription(passwordEvaluation.score) + } +} + +@Composable +fun Suggestion(suggestions: String) { + if (suggestions.isNotEmpty()) { + Text( + text = annotatedStringResource( + R.string.settings_password_suggestions, + suggestions + ), + style = AppTheme.typography.caption1l + ) + } +} + +@Composable +fun PasswordStrengthIndicator(passwordScore: PasswordScore) { + val barLength by animateFloatAsState( + when (passwordScore) { + PasswordScore.Uninitialised -> FIVE_PERCENT + PasswordScore.VeryWeak -> TEN_PERCENT + PasswordScore.Weak -> THIRTY_PERCENT + PasswordScore.Strong -> SIXTY_PERCENT + PasswordScore.VeryStrong -> ONE_HUNDRED_PERCENT + }, + label = "" + ) + val barColor by animateColorAsState( + when (passwordScore) { + PasswordScore.Uninitialised -> AppTheme.colors.red500 + PasswordScore.VeryWeak -> AppTheme.colors.red400 + PasswordScore.Weak -> AppTheme.colors.red300 + PasswordScore.Strong -> AppTheme.colors.yellow500 + PasswordScore.VeryStrong -> AppTheme.colors.green500 + }, + label = "" + ) + + SpacerMedium() + Box( + modifier = Modifier + .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(SizeDefaults.threeQuarter)) + .fillMaxWidth() + ) { + Spacer( + modifier = Modifier + .background(color = barColor, shape = RoundedCornerShape(SizeDefaults.threeQuarter)) + .fillMaxWidth(barLength) + .height(SizeDefaults.threeQuarter) + ) + } + SpacerTiny() +} + +@Composable +fun PasswordStrengthDescription(passwordScore: PasswordScore) { + Row(verticalAlignment = Alignment.CenterVertically) { + when (passwordScore) { + PasswordScore.VeryStrong -> { + Text( + stringResource(R.string.settings_password_very_good), + style = AppTheme.typography.body2l + ) + SpacerTiny() + Icon(Icons.Rounded.Check, null, tint = AppTheme.colors.green600) + } + PasswordScore.Strong -> { + Text( + stringResource(R.string.settings_password_sufficient), + style = AppTheme.typography.body2l + ) + SpacerTiny() + Icon(Icons.Rounded.Check, null, tint = AppTheme.colors.green600) + } + else -> { + Text( + stringResource(R.string.settings_password_not_sufficient), + style = AppTheme.typography.body2l + ) + SpacerTiny() + Icon(Icons.Rounded.Close, null, tint = AppTheme.colors.red600) + } + } + } +} + +@LightDarkPreview +@Composable +fun PasswordStrengthPreview() { + PreviewAppTheme { + PasswordStrength( + modifier = Modifier, + passwordEvaluation = PasswordEvaluation( + PasswordScore.Strong, + "This is a strong password" + ) + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordTextField.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordTextField.kt index f8bb3e71..8564717f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordTextField.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PasswordTextField.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -22,15 +22,14 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.material.Icon import androidx.compose.material.IconButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.TextFieldColors -import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.rounded.Check +import androidx.compose.material3.TextFieldColors import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -54,13 +53,21 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.extensions.disableCopyPasteFromKeyboard +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Requirement( - "O.Data_10#1", "O.Data_11#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "Password field using the keyboard type password. Copying the content is not possible " + - "with this type. Autocorrect is disallowed. It`s not possible to disable third party keyboards." + rationale = "Password field using the keyboard type password. " + + "Autocorrect is disallowed. It`s not possible to disable third party keyboards." +) +@Requirement( + "O.Data_10#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "..using the autofill disabled keyboard options for password entry.", + codeLines = 70 ) @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -74,7 +81,7 @@ fun PasswordTextField( allowAutofill: Boolean = false, allowVisiblePassword: Boolean = false, label: @Composable (() -> Unit)? = null, - colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() + colors: TextFieldColors = erezeptTextFieldColors() ) { var passwordVisible by remember { mutableStateOf(false) } @@ -102,53 +109,56 @@ fun PasswordTextField( val passwordIsNotVisible = stringResource(R.string.password_is_not_visible) val passwordIsVisible = stringResource(R.string.password_is_visible) - OutlinedTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier - .heightIn(min = 56.dp) - .then(autofillModifier) - .semantics { - contentDescription = if (passwordVisible) { - passwordIsVisible - } else { - passwordIsNotVisible + DisableSelection { + ErezeptOutlineText( + value = value, + onValueChange = onValueChange, + modifier = modifier + .heightIn(min = 56.dp) + .then(autofillModifier) + .semantics { + contentDescription = if (passwordVisible) { + passwordIsVisible + } else { + passwordIsNotVisible + } } + .disableCopyPasteFromKeyboard(), + singleLine = true, + keyboardOptions = autofillDisabledPasswordKeyboardOptions(), + keyboardActions = KeyboardActions { + onSubmit() }, - singleLine = true, - keyboardOptions = KeyboardOptions(autoCorrect = false, keyboardType = KeyboardType.Password), - keyboardActions = KeyboardActions { - onSubmit() - }, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - if (isConsistent) { - Icon( - Icons.Rounded.Check, - stringResource(R.string.consistent_password) - ) - } else if (allowVisiblePassword) { - IconButton( - onClick = { passwordVisible = !passwordVisible } - ) { - when (passwordVisible) { - true -> Icon( - Icons.Outlined.Visibility, - stringResource(R.string.settings_password_acc_show_password_toggle) - ) - false -> Icon( - Icons.Outlined.VisibilityOff, - stringResource(R.string.settings_password_acc_show_password_toggle) - ) + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + if (isConsistent) { + Icon( + Icons.Rounded.Check, + stringResource(R.string.consistent_password) + ) + } else if (allowVisiblePassword) { + IconButton( + onClick = { passwordVisible = !passwordVisible } + ) { + when (passwordVisible) { + true -> Icon( + Icons.Outlined.Visibility, + stringResource(R.string.settings_password_show_password_toggle) + ) + false -> Icon( + Icons.Outlined.VisibilityOff, + stringResource(R.string.settings_password_show_password_toggle) + ) + } } } - } - }, - isError = isError, - label = label, - shape = RoundedCornerShape(8.dp), - colors = colors - ) + }, + isError = isError, + label = label, + shape = RoundedCornerShape(SizeDefaults.one), + colors = colors + ) + } } @LightDarkPreview @@ -164,3 +174,13 @@ fun PasswordTextFieldPreview() { ) } } + +@Requirement( + "O.Data_10#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "By disabling autofill we eliminate any recordings that can be done while typing the keyboard." +) +private fun autofillDisabledPasswordKeyboardOptions() = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PreviewAppTheme.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PreviewAppTheme.kt deleted file mode 100644 index 52af6400..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/PreviewAppTheme.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils.compose - -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable -import de.gematik.ti.erp.app.theme.AppTheme - -@Composable -fun PreviewAppTheme(content: @Composable () -> Unit) { - AppTheme { - Surface { - content() - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SaveButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SaveButton.kt new file mode 100644 index 00000000..918928fb --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SaveButton.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun SaveButton( + modifier: Modifier = Modifier, + text: String = stringResource(R.string.generic_save), + isEnabled: Boolean, + onEmpty: () -> Unit, + onClick: () -> Unit +) { + Center( + modifier = Modifier.background(AppTheme.colors.neutral025) + ) { + Button( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = SizeDefaults.tenfold) + .padding(vertical = PaddingDefaults.XXLargeMedium) + .imePadding() + .testTag(TestTag.Profile.EditProfileIcon.EmojiSaveButton), + colors = erezeptButtonColors(), + onClick = if (isEnabled) onClick else onEmpty + ) { + Text( + color = AppTheme.colors.neutral000, + textAlign = TextAlign.Center, + text = text + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ScrollOnFocus.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ScrollOnFocus.kt new file mode 100644 index 00000000..66c70a11 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ScrollOnFocus.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.onFocusChanged +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val LayoutDelay = 330L + +@OptIn(ExperimentalLayoutApi::class) +fun Modifier.scrollOnFocus(to: Int, listState: LazyListState, offset: Int = 0) = composed { + val coroutineScope = rememberCoroutineScope() + val mutex = MutatorMutex() + + var hasFocus by remember { mutableStateOf(false) } + val keyboardVisible = WindowInsets.isImeVisible + + LaunchedEffect(hasFocus, keyboardVisible) { + if (hasFocus && keyboardVisible) { + mutex.mutate { + delay(LayoutDelay) + listState.animateScrollToItem(to, offset) + } + } + } + + onFocusChanged { + if (it.hasFocus) { + hasFocus = true + coroutineScope.launch { + mutex.mutate(MutatePriority.UserInput) { + delay(LayoutDelay) + listState.animateScrollToItem(to, offset) + } + } + } else { + hasFocus = false + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Shimmer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Shimmer.kt new file mode 100644 index 00000000..17032fcf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Shimmer.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall + +// TODO: To be deleted and the ones from ui-components used instead + +private const val LIGHT_ALPHA = 0.2F + +@Deprecated("Use ui-components version") +@Composable +fun LimitedTextShimmer( + background: Color = AppTheme.colors.primary200 +) { + Spacer( + modifier = Modifier + .height(SizeDefaults.oneHalf) + .width(SizeDefaults.fifteenfold) + .background(background) + ) +} + +@Deprecated("Use ui-components version") +@Composable +fun StatusChipShimmer( + background: Color = AppTheme.colors.primary200 +) { + val shape = RoundedCornerShape(SizeDefaults.double) + Row( + Modifier + .background(background, shape) + .padding(SizeDefaults.eighth) + .clip(shape), + verticalAlignment = Alignment.CenterVertically + ) { + TinyTextShimmer() + SpacerSmall() + SquareShapeShimmer(size = SizeDefaults.eighth) + } +} + +@Deprecated("Use ui-components version") +@Composable +fun TinyTextShimmer( + background: Color = AppTheme.colors.primary200 +) { + Spacer( + modifier = Modifier + .height(SizeDefaults.oneHalf) + .width(SizeDefaults.sevenfoldAndHalf) + .background(background) + ) +} + +@Deprecated("Use ui-components version") +@Composable +fun RowTextShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200 +) { + Spacer( + modifier = Modifier + .height(SizeDefaults.oneHalf) + .fillMaxWidth() + .then(modifier) + .background(background) + ) +} + +@Deprecated("Use ui-components version") +@Composable +fun CircularShapeShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + alpha: Float = LIGHT_ALPHA, + size: Dp = SizeDefaults.fivefold +) { + Box( + modifier = Modifier + .then(modifier) + .size(size) + .clip(CircleShape) + .background(background) + .alpha(alpha) + ) +} + +@Deprecated("Use ui-components version") +@Composable +fun SquareShapeShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + alpha: Float = LIGHT_ALPHA, + size: Dp = SizeDefaults.fivefold +) { + Box( + modifier = Modifier + .then(modifier) + .size(size) + .clip(RoundedCornerShape(SizeDefaults.one)) + .background(background) + .alpha(alpha) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Spacer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Spacer.kt deleted file mode 100644 index 85e4fb13..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Spacer.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils.compose - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import de.gematik.ti.erp.app.theme.PaddingDefaults - -@Composable -fun SpacerTiny() = - Spacer(modifier = Modifier.size(PaddingDefaults.Tiny)) - -@Composable -fun SpacerSmall() = - Spacer(modifier = Modifier.size(PaddingDefaults.Small)) - -@Composable -fun SpacerMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.Medium)) - -@Composable -fun SpacerShortMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.ShortMedium)) - -@Composable -fun SpacerLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.Large)) - -@Composable -fun SpacerXLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.XLarge)) - -@Composable -fun SpacerXXLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.XXLarge)) - -@Composable -fun SpacerXXLargeMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.XXLargeMedium)) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SwitchWithText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SwitchWithText.kt index 5b0000e5..1f899a34 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SwitchWithText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/SwitchWithText.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Switch import androidx.compose.material.Text @@ -34,12 +35,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.preview.PreviewAppTheme @Composable -fun SwitchWithText( +fun SwitchLeftWithText( modifier: Modifier = Modifier, text: String, checked: Boolean, @@ -73,11 +77,60 @@ fun SwitchWithText( } } +@Composable +fun SwitchRightWithText( + modifier: Modifier = Modifier, + text: String, + checked: Boolean, + enabled: Boolean = true, + onCheckedChange: (Boolean) -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(PaddingDefaults.Medium)) + .toggleable( + value = checked, + onValueChange = onCheckedChange, + enabled = enabled, + role = Role.Switch, + interactionSource = interactionSource, + indication = LocalIndication.current + ) + .padding(PaddingDefaults.Medium), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + style = AppTheme.typography.body1, + text = text + ) + Switch( + checked = checked, + onCheckedChange = null, + enabled = enabled + ) + } +} + +@LightDarkPreview +@Composable +internal fun SwitchLeftWithTextPreview() { + PreviewAppTheme { + SwitchLeftWithText( + text = "Text added to be shown with a switch", + checked = false, + onCheckedChange = {} + ) + } +} + @LightDarkPreview @Composable -fun SwitchWithTextPreview() { +internal fun SwitchRightWithTextPreview() { PreviewAppTheme { - SwitchWithText( + SwitchRightWithText( text = "Text added to be shown with a switch", checked = false, onCheckedChange = {} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Text.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Text.kt new file mode 100644 index 00000000..64179ac1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Text.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.theme.AppTheme + +@Composable +fun Body1Text(text: String) { + Text(text = text, style = AppTheme.typography.body1) +} + +@Composable +fun Body1lText(text: String) { + Text(text = text, style = AppTheme.typography.body1l) +} + +@Composable +fun Body2Text(text: String) { + Text(text = text, style = AppTheme.typography.body2) +} + +@Composable +fun Body2lText(text: String) { + Text(text = text, style = AppTheme.typography.body2l) +} + +@Composable +fun Subtitle1Text(text: String) { + Text(text = text, style = AppTheme.typography.subtitle1) +} + +@Composable +fun Subtitle1lText(text: String) { + Text(text = text, style = AppTheme.typography.subtitle1l) +} + +@Composable +fun Subtitle2Text(text: String) { + Text(text = text, style = AppTheme.typography.subtitle2) +} + +@Composable +fun Subtitle2lText(text: String) { + Text(text = text, style = AppTheme.typography.subtitle2l) +} + +@Composable +fun Caption1Text(text: String) { + Text(text = text, style = AppTheme.typography.caption1) +} + +@Composable +fun Caption1lText(text: String) { + Text(text = text, style = AppTheme.typography.caption1l) +} + +@Composable +fun Caption2Text(text: String) { + Text(text = text, style = AppTheme.typography.caption2) +} + +@Composable +fun H1Text(text: String) { + Text(text = text, style = AppTheme.typography.h1) +} + +@Composable +fun H2Text(text: String) { + Text(text = text, style = AppTheme.typography.h2) +} + +@Composable +fun H3Text(text: String) { + Text(text = text, style = AppTheme.typography.h3) +} + +@Composable +fun H4Text(text: String) { + Text(text = text, style = AppTheme.typography.h4) +} + +@Composable +fun H5Text(text: String) { + Text(text = text, style = AppTheme.typography.h5) +} + +@Composable +fun H6Text(text: String) { + Text(text = text, style = AppTheme.typography.h6) +} + +@Composable +fun ButtonText(text: String) { + Text(text = text, style = AppTheme.typography.button) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt index eeb36968..4fa686bc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -58,6 +58,8 @@ typealias TimeDescriptionFormatter = (diff: TimeDiff, localDt: LocalDateTime, du @Composable fun timeDescription( instant: Instant, + now: Instant = Clock.System.now(), + timeZone: TimeZone = TimeZone.currentSystemDefault(), formatter: TimeDescriptionFormatter = TimeDescriptionDefaults.formatter() ): State { LocalConfiguration.current @@ -65,9 +67,9 @@ fun timeDescription( val dt by rememberUpdatedState(instant) val fmt by rememberUpdatedState(formatter) val timeString = remember(dt, fmt) { - val duration = Clock.System.now() - dt + val duration = now - dt val diffMinutes = duration.inWholeMinutes - val localDt = dt.toLocalDateTime(TimeZone.currentSystemDefault()) + val localDt = dt.toLocalDateTime(timeZone) mutableStateOf(fmt(timeDiff(diffMinutes = diffMinutes), localDt, duration)) } return timeString diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt index e4775b8b..a0be4e04 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt index ee4a8eb1..a565de64 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -38,6 +38,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall @Composable fun Toggle( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/UiStateMachine.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/UiStateMachine.kt index bd36fa90..6e5c31e0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/UiStateMachine.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/UiStateMachine.kt @@ -1,26 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.animated.AnimationTime import de.gematik.ti.erp.app.utils.uistate.UiState -import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmpty +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState @Composable fun UiStateMachine( @@ -30,12 +33,16 @@ fun UiStateMachine( onError: (@Composable (Throwable) -> Unit)? = null, onContent: @Composable (T) -> Unit ) { - val data = state.data - val error = state.error - when { - state.isLoading -> onLoading?.invoke() - error != null -> onError?.invoke(error) - data == null || state.isEmpty -> onEmpty?.invoke() - else -> onContent(data) + Crossfade( + targetState = state, + animationSpec = tween(AnimationTime.DELAY_100), + label = "ui-state-change" + ) { currentState -> + when { + currentState.isLoading -> onLoading?.invoke() + currentState.error != null -> onError?.invoke(currentState.error) + currentState.data == null || state.isEmptyState -> onEmpty?.invoke() + else -> onContent(currentState.data) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt index 48c55561..908ef8aa 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/presentation/PasswordFieldsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/presentation/PasswordFieldsController.kt new file mode 100644 index 00000000..1861fd96 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/presentation/PasswordFieldsController.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.nulabinc.zxcvbn.StandardDictionaries +import com.nulabinc.zxcvbn.Zxcvbn +import com.nulabinc.zxcvbn.ZxcvbnBuilder +import com.nulabinc.zxcvbn.io.Resource +import com.nulabinc.zxcvbn.matchers.DictionaryLoader +import de.gematik.ti.erp.app.base.Controller +import de.gematik.ti.erp.app.utils.compose.PasswordEvaluation +import de.gematik.ti.erp.app.utils.compose.PasswordScore +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +const val TIMEOUT = 5000L + +open class PasswordFieldsController( + private val passwordStrengthEvaluator: Zxcvbn +) : Controller() { + private val _password = MutableStateFlow("") + private val _repeatedPassword = MutableStateFlow("") + + val passwordFieldsState = combine( + _password, + _repeatedPassword + ) { + password, repeatedPassword -> + val passwordEvaluation = evaluatePassword(password) + PasswordFieldsData( + password = password, + repeatedPassword = repeatedPassword, + passwordEvaluation = passwordEvaluation, + repeatedPasswordHasError = password.isNotBlank() && repeatedPassword.isNotBlank() && !password.startsWith(repeatedPassword), + passwordIsValidAndConsistent = password.isNotBlank() && password == repeatedPassword && passwordEvaluation.isStrongEnough + ) + }.stateIn( + controllerScope, + SharingStarted.WhileSubscribed(TIMEOUT), + PasswordFieldsData( + "", + "", + PasswordEvaluation( + PasswordScore.Uninitialised, + "" + ), + false, + false + ) + ) + + @Suppress("MagicNumber") + private fun evaluatePassword(password: String): PasswordEvaluation { + val passwordStrengthEvaluation = passwordStrengthEvaluator.measure(password) + return when (passwordStrengthEvaluation.score) { + 0 -> PasswordEvaluation( + PasswordScore.Uninitialised, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + 1 -> PasswordEvaluation( + PasswordScore.VeryWeak, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + 2 -> PasswordEvaluation( + PasswordScore.Weak, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + 3 -> PasswordEvaluation( + PasswordScore.Strong, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + 4 -> PasswordEvaluation( + PasswordScore.VeryStrong, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + else -> PasswordEvaluation( + PasswordScore.Uninitialised, + passwordStrengthEvaluation.feedback.suggestions.joinToString("\n").trim() + ) + } + } + + fun onPasswordChange(password: String) { + _password.update { password } + _repeatedPassword.update { "" } + } + + fun onRepeatedPasswordChange(repeatedPassword: String) { + _repeatedPassword.update { repeatedPassword } + } +} + +data class PasswordFieldsData( + val password: String, + val repeatedPassword: String, + val passwordEvaluation: PasswordEvaluation, + val repeatedPasswordHasError: Boolean, + val passwordIsValidAndConsistent: Boolean +) + +@Composable +fun rememberPasswordFieldsController(): PasswordFieldsController { + val context = LocalContext.current + val assetManager = context.assets + val germanDictionaryFile = assetManager.open("german_dictionary.txt") + val germanDictionaryResource = Resource { germanDictionaryFile } + val passwordStrengthEvaluator = ZxcvbnBuilder().dictionaries( + StandardDictionaries.loadAllDictionaries() + ) + .dictionary(DictionaryLoader("german_dictionary", germanDictionaryResource).load()) + .build() + + return remember { + PasswordFieldsController( + passwordStrengthEvaluator = passwordStrengthEvaluator + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CardUnlockNewSecretScreenPreviewData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CardUnlockNewSecretScreenPreviewData.kt new file mode 100644 index 00000000..f87ebefa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CardUnlockNewSecretScreenPreviewData.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +data class CardUnlockNewSecretScreenPreviewData( + val pin: String, + val repeatedNewPin: String, + val isConsistent: Boolean +) + +class CardUnlockNewSecretScreenPreviewDataProvider : PreviewParameterProvider { + override val values = sequenceOf( + CardUnlockNewSecretScreenPreviewData("", "", false), + CardUnlockNewSecretScreenPreviewData("123123", "123123", true), + CardUnlockNewSecretScreenPreviewData("123123", "123", false), + CardUnlockNewSecretScreenPreviewData("123123", "", false) + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CommonPreviewProviders.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CommonPreviewProviders.kt new file mode 100644 index 00000000..fdf582e4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/CommonPreviewProviders.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.card.model.command.UnlockMethod + +class BooleanPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf(true, false) +} + +data class CanBoolean(val can: String, val boolean: Boolean) + +class CanBooleanPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + CanBoolean("123123", false), + CanBoolean("123123", true), + CanBoolean("", false), + CanBoolean("", true) + ) +} + +class UnlockMethodPreviewParameterProvider : PreviewParameterProvider { + override val values = UnlockMethod.entries.asSequence() +} + +data class Pin(val pin: String) + +class PinPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + Pin(""), + Pin("123"), + Pin("123456"), + Pin("123456789"), + Pin("123 123") + ) +} + +class PukPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + "", + "123", + "123123", + "123123123", + "123 123" + ) +} + +class CanPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + "", + "123", + "123123", + "123123123", + "123 123" + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/LanguageCodePreviewParameterProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/LanguageCodePreviewParameterProvider.kt new file mode 100644 index 00000000..1eeabc98 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/LanguageCodePreviewParameterProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +class LanguageCodePreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf("de", "en", "öü", "es") +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/PreviewAppTheme.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/PreviewAppTheme.kt new file mode 100644 index 00000000..594be6ad --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/PreviewAppTheme.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.preview + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.base.BaseActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.LocalTimeZone +import de.gematik.ti.erp.app.theme.AppTheme +import kotlinx.datetime.TimeZone + +@Composable +fun PreviewAppTheme( + modifier: Modifier = Modifier, + content: @Composable () -> Unit + +) { + CompositionLocalProvider( + LocalActivity provides BaseActivity(), + LocalTimeZone provides TimeZone.of("Europe/Berlin") + ) { + AppTheme { + Surface( + modifier = modifier + ) { + content() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/TestScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/TestScaffold.kt new file mode 100644 index 00000000..8449ece4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/preview/TestScaffold.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.preview + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.LightDarkPreview +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar + +@Composable +fun TestScaffold( + modifier: Modifier = Modifier, + scaffoldState: ScaffoldState = rememberScaffoldState(), + topBarColor: Color = MaterialTheme.colors.surface, + navigationMode: NavigationBarMode? = NavigationBarMode.Close, + bottomBar: @Composable () -> Unit = {}, + topBarTitle: String, + elevated: Boolean = false, + onBack: () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + modifier = modifier, + scaffoldState = scaffoldState, + topBar = { + NavigationTopAppBar( + navigationMode = navigationMode, + backgroundColor = topBarColor, + title = topBarTitle, + elevation = if (elevated) { + AppBarDefaults.TopAppBarElevation + } else { + SizeDefaults.zero + }, + onBack = onBack, + actions = actions + ) + }, + bottomBar = bottomBar, + content = content + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun TestScaffoldPreview( + @PreviewParameter(NavigationBarModePreviewParameters::class) modes: NavigationBarMode +) { + PreviewAppTheme { + TestScaffold( + topBarTitle = "TestTitle", + navigationMode = modes + ) { + } + } +} + +class NavigationBarModePreviewParameters : PreviewParameterProvider { + override val values: Sequence + get() = NavigationBarMode.entries.asSequence().plus(null) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/BuildConfigExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/BuildConfigExtension.kt index 980b261b..8911daff 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/BuildConfigExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/BuildConfigExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt index 53a32aba..662d9bcc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions @@ -27,10 +27,21 @@ import android.os.Build import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.fhir.model.Coordinates +import de.gematik.ti.erp.app.pharmacy.ui.model.MapContent +import de.gematik.ti.erp.app.utils.compose.canHandleIntent +import de.gematik.ti.erp.app.utils.compose.provideEmailIntent +import io.github.aakira.napier.Napier fun Context.isGooglePlayServiceAvailable(): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + try { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS + } catch (e: Throwable) { + // silently fail + Napier.e { "isGooglePlayServiceAvailable failed: ${e.message}" } + false + } fun Context.hasNFCTerminal(): Boolean = this.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC) @@ -50,3 +61,41 @@ fun Context.openAppPlayStoreLink() { ) ) } + +fun Context.gotoCoordinates(coordinates: Coordinates) { + val (component, mapIntent) = mapsSelectionLauncher(coordinates) + when { + component != null -> startActivity(mapIntent) + else -> startActivity(openGoogleMaps(coordinates).mapIntent) + } +} + +fun Context.openEmailClient(emailAddress: String) { + val intent = provideEmailIntent(emailAddress) + if (canHandleIntent(intent, packageManager)) { + startActivity(intent) + } +} + +private fun Context.openGoogleMaps(coordinates: Coordinates): MapContent { + val uri = Uri.parse( + "https://www.google.com/maps/dir/?api=1&destination=${coordinates.latitude},${coordinates.longitude}" + ) + val mapIntent = Intent(Intent.ACTION_VIEW, uri) + mapIntent.setPackage("com.google.android.apps.maps") + return MapContent( + component = mapIntent.resolveActivity(packageManager), + mapIntent = mapIntent + ) +} + +private fun Context.mapsSelectionLauncher(coordinates: Coordinates): MapContent { + val uri = Uri.parse( + "https://www.google.com/maps/dir/?api=1&destination=${coordinates.latitude},${coordinates.longitude}" + ) + val mapIntent = Intent(Intent.ACTION_VIEW, uri) + return MapContent( + component = mapIntent.resolveActivity(packageManager), + mapIntent = mapIntent + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt index 6d2dccbe..b088ace1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions @@ -33,6 +33,8 @@ import java.time.format.FormatStyle val dateTimeShortFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) +val dateTimeHHMMFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") + fun dateTimeShortText(instant: Instant): String = instant.toLocalDateTime(TimeZone.currentSystemDefault()) .toJavaLocalDateTime() @@ -76,3 +78,8 @@ fun temporalText(temporal: FhirTemporal, timeZone: TimeZone = TimeZone.UTC): Str else -> "n.a." } + +object DateTimeUtils { + val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt index 0ede4fae..b8070219 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ErezeptKeyboardOptions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ErezeptKeyboardOptions.kt new file mode 100644 index 00000000..abf9c6dd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ErezeptKeyboardOptions.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType + +object ErezeptKeyboardOptions { + + val numberPassword = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.NumberPassword + ) + + val number = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number + ) + + val defaultDone = KeyboardOptions(imeAction = ImeAction.Done) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt index 9c5a2dfa..5e492ca5 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/KeyboardExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/KeyboardExtensions.kt new file mode 100644 index 00000000..2a4ca6f3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/KeyboardExtensions.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import android.view.View +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.core.view.WindowInsetsCompat +import kotlinx.coroutines.delay + +const val KEYBOARD_WAIT_TIME = 500L + +val View.isKeyboardVisible: Boolean + get() = WindowInsetsCompat + .toWindowInsetsCompat(rootWindowInsets) + .isVisible(WindowInsetsCompat.Type.ime()) + +/** + * @return State true if the keyboard is visible + */ +@Composable +fun keyboardAsState(): State { + val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0 + return rememberUpdatedState(isImeVisible) +} + +suspend fun SoftwareKeyboardController.showKeyboardOnNotOpen(isKeyboardOpen: Boolean) { + delay(KEYBOARD_WAIT_TIME) + if (!isKeyboardOpen) { + show() + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ListExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ListExtensions.kt new file mode 100644 index 00000000..b30e5651 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ListExtensions.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +infix fun List.doesNotContain(item: E?) = contains(item).not() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/LocaleExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/LocaleExtension.kt index 067ba426..b4d504b1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/LocaleExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/LocaleExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ModifierExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ModifierExtension.kt new file mode 100644 index 00000000..8c4c0d7e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ModifierExtension.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animateValue +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.navigation.oneSecondInfiniteTween +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val LayoutDelay = 330L + +/** + * https://google.github.io/accompanist/insets/ + */ +fun Modifier.navigationBarsWithImePadding() = this + .navigationBarsPadding() + .imePadding() + +/** + * When the keyboard is open it provides the exact height of the keyboard in Dp + * or 0 Dp when the keyboard is closed + */ +@Composable +fun rememberImeHeight(): Dp { + val density = LocalDensity.current + val insets = WindowInsets.ime + return remember(insets.getBottom(density)) { + with(density) { insets.getBottom(density).toDp() } + } +} + +@Composable +fun Modifier.imeHeight(): Modifier { + val density = LocalDensity.current + val insets = WindowInsets.ime + val height = remember(insets.getBottom(density)) { + with(density) { insets.getBottom(density).toDp() } + } + return this.padding(bottom = height) +} + +@Composable +fun paddingKeyboardHeight(): PaddingValues = WindowInsets.ime.asPaddingValues(LocalDensity.current) + +@OptIn(ExperimentalLayoutApi::class) +fun Modifier.animateScrollOnFocus(to: Int, listState: LazyListState, offset: Int = 0) = composed { + val coroutineScope = rememberCoroutineScope() + val mutex = MutatorMutex() + + var hasFocus by remember { mutableStateOf(false) } + val keyboardVisible = WindowInsets.isImeVisible + + LaunchedEffect(hasFocus, keyboardVisible) { + if (hasFocus && keyboardVisible) { + mutex.mutate { + delay(LayoutDelay) + listState.animateScrollToItem(to, offset) + } + } + } + + onFocusChanged { + if (it.hasFocus) { + hasFocus = true + coroutineScope.launch { + mutex.mutate(MutatePriority.UserInput) { + delay(LayoutDelay) + listState.animateScrollToItem(to, offset) + } + } + } else { + hasFocus = false + } + } +} + +@Composable +fun Modifier.greyCircularBorder(): Modifier = this.then( + Modifier.border(SizeDefaults.eighth, AppTheme.colors.neutral300, CircleShape) +) + +@Composable +fun Modifier.circularBorder( + color: Color, + width: Dp = SizeDefaults.eighth +): Modifier = this.then( + Modifier.border( + color = color, + width = width, + shape = CircleShape + ) +) + +@Composable +fun Modifier.animatedCircularBorder( + animate: Boolean, + startColor: Color = AppTheme.colors.neutral400, + endColor: Color = AppTheme.colors.neutral600 +): Modifier { + return if (animate) { + val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition") + val width by infiniteTransition.animateValue( + initialValue = SizeDefaults.zero, + targetValue = SizeDefaults.half, + typeConverter = Dp.VectorConverter, + animationSpec = oneSecondInfiniteTween(), + label = "width" + ) + val color by infiniteTransition.animateColor( + initialValue = startColor, + targetValue = endColor, + animationSpec = oneSecondInfiniteTween(), + label = "color" + ) + circularBorder( + color = color, + width = width + ) + } else { + Modifier + } +} + +fun Modifier.identifyItemOnScroll( + windowHeightPx: Int, + isVisible: (Boolean) -> Unit +) = + this.then( + Modifier.onGloballyPositioned { layoutCoordinates -> + try { + val itemPosition = layoutCoordinates.localToWindow(Offset.Zero) + val itemHeight = layoutCoordinates.size.height + isVisible((itemPosition.y + itemHeight) > 0 && itemPosition.y < windowHeightPx) + } catch (e: Throwable) { + isVisible(false) + } + } + ) + +fun Modifier.disableCopyPasteFromKeyboard() = this.then( + Modifier.onPreviewKeyEvent { keyEvent -> + if ((keyEvent.key == Key.C && keyEvent.isCtrlPressed) || + (keyEvent.key == Key.X && keyEvent.isCtrlPressed) || + (keyEvent.key == Key.V && keyEvent.isCtrlPressed) + ) { + // Intercept copy, cut, and paste actions + return@onPreviewKeyEvent true + } + false + } +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ScreenHeightInDp.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ScreenHeightInDp.kt new file mode 100644 index 00000000..7fb6fa0a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ScreenHeightInDp.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.window.layout.WindowMetricsCalculator + +@Composable +fun screenHeightInDp(): Dp { + val context = LocalContext.current + val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) + // Obtain the screen height in pixels + val screenHeightPx = metrics.bounds.height() + // Convert pixels to dp + val density = LocalDensity.current + return with(density) { screenHeightPx.toDp() } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt index 072ea3ea..c67d2ceb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt @@ -1,30 +1,36 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.compose.material.SnackbarDuration +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import com.google.android.material.snackbar.Snackbar import de.gematik.ti.erp.app.features.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @Stable interface SnackbarScaffold { @@ -38,8 +44,52 @@ interface SnackbarScaffold { ) } +@Deprecated("Use LocalSnackbarScaffold instead", ReplaceWith("LocalSnackbarScaffold")) val LocalSnackbar: SnackbarScaffoldCompositionLocal = SnackbarScaffoldCompositionLocal +val LocalSnackbarScaffold = staticCompositionLocalOf { error("No snackbar provided!") } + +fun SnackbarHostState.dismiss() = currentSnackbarData?.dismiss() + +fun SnackbarHostState.showWithDismissButton( + message: String, + actionLabel: String, + duration: SnackbarDuration = SnackbarDuration.Short, + scope: CoroutineScope +) { + scope.launch { + val result = showSnackbar( + message = message, + duration = duration, + actionLabel = actionLabel + ) + + when (result) { + SnackbarResult.Dismissed -> dismiss() + SnackbarResult.ActionPerformed -> dismiss() + } + } +} + +fun SnackbarHostState.show( + message: String, + scope: CoroutineScope +) { + scope.launch { showSnackbar(message = message) } +} + +fun SnackbarHostState.showIndefinite( + message: String, + scope: CoroutineScope +) { + scope.launch { + showSnackbar( + message = message, + duration = SnackbarDuration.Indefinite + ) + } +} + object SnackbarScaffoldCompositionLocal { val current: SnackbarScaffold @Composable diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/StringExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/StringExtensions.kt index 7569851b..6853a621 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/StringExtensions.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/StringExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions @@ -24,3 +24,9 @@ fun String.capitalizeFirstChar(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + +fun String.toFormattedFloat(): Float? { + return this.split("-")[0] + .replace(".", "") + .toFloatOrNull() +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SwitchExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SwitchExtensions.kt new file mode 100644 index 00000000..8077fdfa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SwitchExtensions.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.theme.AppTheme + +@Composable +fun SwitchDefaults.erezeptColors() = colors( + checkedTrackColor = AppTheme.colors.primary200, + uncheckedTrackColor = AppTheme.colors.neutral200, + checkedThumbColor = AppTheme.colors.primary600, + uncheckedThumbColor = AppTheme.colors.neutral400, + checkedBorderColor = AppTheme.colors.primary600, + uncheckedBorderColor = AppTheme.colors.neutral400 +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt index d7721e5c..b65bb6be 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt @@ -1,25 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions import kotlin.streams.asSequence +@Suppress("MagicNumber") // see https://unicode.org/emoji/charts/full-emoji-list.html private fun Int.isEmoticon() = this in 0x1F600..0xE007F diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TryExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TryExtension.kt index 83ca095b..04c9153f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TryExtension.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TryExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.extensions diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/UriHandlerExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/UriHandlerExtension.kt new file mode 100644 index 00000000..4dc057ed --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/UriHandlerExtension.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.extensions + +import android.net.Uri +import androidx.compose.ui.platform.UriHandler +import io.github.aakira.napier.Napier + +fun UriHandler.openUriWhenValid(uri: String) { + if (isValidUri(uri)) { + openUri(uri) + } +} + +private fun isValidUri(uriString: String): Boolean { + return try { + val uri = Uri.parse(uriString) + uri != null && uri.scheme != null + } catch (e: Throwable) { + Napier.e { "uri $uriString is not valid ${e.stackTraceToString()}" } + false + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/uistate/UiState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/uistate/UiState.kt index 86b46810..db8d71a9 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/uistate/UiState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/uistate/UiState.kt @@ -1,27 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.uistate -import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -@Immutable +@Stable data class UiState( val isLoading: Boolean = false, @Stable val data: T? = null, @@ -50,7 +49,19 @@ data class UiState( @Suppress("FunctionName") fun Error(error: Throwable): UiState = UiState(isLoading = false, error = error) - val UiState<*>.isEmpty: Boolean + val UiState<*>.isEmptyState: Boolean get() = !isLoading && isDataEmpty + + val UiState<*>.isNotDataState: Boolean + get() = !isDataState + + val UiState<*>.isErrorState: Boolean + get() = !isLoading && error != null && data == null + + val UiState<*>.isDataState: Boolean + get() = !isLoading && data != null && error == null + + val UiState<*>.isLoadingState: Boolean + get() = isLoading && data == null && error == null } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt index e4e54c83..bab2e1ad 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt index 3e5d42f3..ef490f79 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.interceptor @@ -29,11 +29,11 @@ import de.gematik.ti.erp.app.secureRandomInstance import de.gematik.ti.erp.app.vau.VauChannelSpec import de.gematik.ti.erp.app.vau.VauCryptoConfig import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase +import io.github.aakira.napier.Napier import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Response -import io.github.aakira.napier.Napier import java.io.IOException import java.net.HttpURLConnection.HTTP_FORBIDDEN import java.net.HttpURLConnection.HTTP_UNAUTHORIZED @@ -55,7 +55,6 @@ class VauException(e: Exception) : IOException(e) @Requirement( "A_20161-01#4", - "A_20174#1 ", sourceSpecification = "gemSpec_Krypt", rationale = "Handle VAU response." ) @@ -66,6 +65,17 @@ class VauChannelInterceptor( private val dispatchers: DispatchProvider, private val networkSecPrefs: SharedPreferences ) : Interceptor { + @Requirement( + "A_20175#2", + "A_20174#6", + sourceSpecification = "gemSpec_Krypt", + rationale = "Set/Retrieve a UserPseudonym from secure storage" + ) + @Requirement( + "A_20161-01#13", + sourceSpecification = "gemSpec_Krypt", + rationale = "7. VAU-Endpoint respects userpseudonym if present" + ) private var previousUserAlias = networkSecPrefs.getString(VAU_USER_ALIAS_PREF_KEY, null) ?: "0" set(v) { field = v @@ -96,7 +106,11 @@ class VauChannelInterceptor( // outer response val encryptedResponse = chain.proceed(encryptedRequest.first) - + @Requirement( + "A_20174#7", + sourceSpecification = "gemSpec_Krypt", + rationale = "handle encrypted response and user pseudonym" + ) return if (!encryptedResponse.isSuccessful) { // e.g. 401 -> user pseudonym unknown -> reset to zero if (encryptedResponse.code == HTTP_UNAUTHORIZED || encryptedResponse.code == HTTP_FORBIDDEN) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt index 5788f4b2..55cdfdfd 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.webview import android.content.Intent @@ -29,9 +31,10 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -47,7 +50,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.updateLayoutParams import androidx.webkit.WebViewAssetLoader import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffoldWithScrollState import de.gematik.ti.erp.app.utils.compose.NavigationBarMode const val URI_TERMS_OF_USE = "file:///android_asset/terms_of_use.html" @@ -55,9 +58,8 @@ const val URI_TERMS_OF_USE = "file:///android_asset/terms_of_use.html" @Requirement( "O.Arch_8#1", "O.Plat_11#1", - "O.Plat_14", sourceSpecification = "BSI-eRp-ePA", - rationale = "Webview containing local html without javascript. No cookies are created." + rationale = "Webviews containing local html without javascript. No cookies are created." ) @Composable fun WebViewScreen( @@ -67,17 +69,21 @@ fun WebViewScreen( navigationMode: NavigationBarMode = NavigationBarMode.Back, onBack: () -> Unit ) { - var scrollState by remember { mutableStateOf(0) } - AnimatedElevationScaffold( + var scrollState by remember { mutableIntStateOf(0) } + AnimatedElevationScaffoldWithScrollState( + modifier = modifier, + topBarTitle = title, elevated = scrollState > 0, navigationMode = navigationMode, bottomBar = {}, actions = {}, - topBarTitle = title, - onBack = onBack, - modifier = modifier + onBack = onBack ) { - WebView(Modifier.fillMaxSize(), url, onScroll = { scrollState = it }) + WebView( + modifier = Modifier.fillMaxSize(), + url = url, + onScroll = { scrollState = it } + ) } } @@ -87,12 +93,19 @@ private fun WebView( url: String, onScroll: (y: Int) -> Unit ) { + @Requirement( + "O.Arch_8#2", + "O.Plat_11#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Javascript is disabled on all webviews used in the app." + ) val context = LocalContext.current val colors = MaterialTheme.colors val typo = MaterialTheme.typography val webView = remember(colors, typo) { WebView(context).apply { setBackgroundColor(colors.background.toArgb()) + settings.javaScriptCanOpenWindowsAutomatically = false settings.javaScriptEnabled = false setOnScrollChangeListener { _, _, scrollY, _, _ -> onScroll(scrollY) } webViewClient = createWebViewClient(colors, typo) @@ -105,6 +118,7 @@ private fun WebView( onDispose { webView.clearCache(true) webView.clearHistory() + webView.destroy() } } @@ -113,10 +127,7 @@ private fun WebView( webView.loadUrl(url) webView }, - modifier = modifier - .onSizeChanged { - size = it - } + modifier = modifier.onSizeChanged { size = it } ) { it.updateLayoutParams { this.height = size.width @@ -200,7 +211,7 @@ fun createWebViewClient(colors: Colors, typo: Typography) = object : WebViewClie } @Requirement( - "O.Plat_13", + "O.Plat_10#1", sourceSpecification = "BSI-eRp-ePA", rationale = "Disables unused schemes" ) diff --git a/app/features/src/main/res/drawable-xhdpi/afternoon.webp b/app/features/src/main/res/drawable-xhdpi/afternoon.webp new file mode 100644 index 00000000..ac19a4b7 Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/afternoon.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/dm_code.webp b/app/features/src/main/res/drawable-xhdpi/dm_code.webp new file mode 100644 index 00000000..364363d7 Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/dm_code.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/evening.webp b/app/features/src/main/res/drawable-xhdpi/evening.webp new file mode 100644 index 00000000..cfb1d6a2 Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/evening.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/morning.webp b/app/features/src/main/res/drawable-xhdpi/morning.webp new file mode 100644 index 00000000..46a773bf Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/morning.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/noon.webp b/app/features/src/main/res/drawable-xhdpi/noon.webp new file mode 100644 index 00000000..00446b73 Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/noon.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/pharmacy_small_32.webp b/app/features/src/main/res/drawable-xhdpi/pharmacy_small_32.webp new file mode 100644 index 00000000..378d5ab0 Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/pharmacy_small_32.webp differ diff --git a/app/features/src/main/res/drawable-xhdpi/time.webp b/app/features/src/main/res/drawable-xhdpi/time.webp new file mode 100644 index 00000000..93eed6be Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/time.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/afternoon.webp b/app/features/src/main/res/drawable-xxhdpi/afternoon.webp new file mode 100644 index 00000000..488f4abe Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/afternoon.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/dm_code.webp b/app/features/src/main/res/drawable-xxhdpi/dm_code.webp new file mode 100644 index 00000000..a38baeec Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/dm_code.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/evening.webp b/app/features/src/main/res/drawable-xxhdpi/evening.webp new file mode 100644 index 00000000..59080ecc Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/evening.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/morning.webp b/app/features/src/main/res/drawable-xxhdpi/morning.webp new file mode 100644 index 00000000..1fa62a4c Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/morning.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/noon.webp b/app/features/src/main/res/drawable-xxhdpi/noon.webp new file mode 100644 index 00000000..79871888 Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/noon.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/pharmacy_small_32.webp b/app/features/src/main/res/drawable-xxhdpi/pharmacy_small_32.webp new file mode 100644 index 00000000..a7779a79 Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/pharmacy_small_32.webp differ diff --git a/app/features/src/main/res/drawable-xxhdpi/time.webp b/app/features/src/main/res/drawable-xxhdpi/time.webp new file mode 100644 index 00000000..7d12e929 Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/time.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/afternoon.webp b/app/features/src/main/res/drawable-xxxhdpi/afternoon.webp new file mode 100644 index 00000000..ec99cd89 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/afternoon.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/dm_code.webp b/app/features/src/main/res/drawable-xxxhdpi/dm_code.webp new file mode 100644 index 00000000..2796dd61 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/dm_code.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/evening.webp b/app/features/src/main/res/drawable-xxxhdpi/evening.webp new file mode 100644 index 00000000..c16eed49 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/evening.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/morning.webp b/app/features/src/main/res/drawable-xxxhdpi/morning.webp new file mode 100644 index 00000000..90a159dc Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/morning.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/noon.webp b/app/features/src/main/res/drawable-xxxhdpi/noon.webp new file mode 100644 index 00000000..6bbf9129 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/noon.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/pharmacy_small_32.webp b/app/features/src/main/res/drawable-xxxhdpi/pharmacy_small_32.webp new file mode 100644 index 00000000..e7cc9f27 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/pharmacy_small_32.webp differ diff --git a/app/features/src/main/res/drawable-xxxhdpi/time.webp b/app/features/src/main/res/drawable-xxxhdpi/time.webp new file mode 100644 index 00000000..11187561 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/time.webp differ diff --git a/app/features/src/main/res/drawable/gematik_logo_flag_with_background.png b/app/features/src/main/res/drawable/gematik_logo_flag_with_background.png new file mode 100644 index 00000000..a3c92931 Binary files /dev/null and b/app/features/src/main/res/drawable/gematik_logo_flag_with_background.png differ diff --git a/app/features/src/main/res/drawable/ic_delete_icon.xml b/app/features/src/main/res/drawable/ic_delete_icon.xml new file mode 100644 index 00000000..aadbfb6d --- /dev/null +++ b/app/features/src/main/res/drawable/ic_delete_icon.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/features/src/main/res/drawable/ic_map_location.webp b/app/features/src/main/res/drawable/ic_map_location.webp new file mode 100644 index 00000000..d3614a9a Binary files /dev/null and b/app/features/src/main/res/drawable/ic_map_location.webp differ diff --git a/app/features/src/main/res/drawable/ic_qr_code.xml b/app/features/src/main/res/drawable/ic_qr_code.xml index 6d5ebdfb..adcd2d12 100644 --- a/app/features/src/main/res/drawable/ic_qr_code.xml +++ b/app/features/src/main/res/drawable/ic_qr_code.xml @@ -1,6 +1,6 @@ diff --git a/app/features/src/main/res/raw/health_insurance_contacts.json b/app/features/src/main/res/raw/health_insurance_contacts.json index 71badfdc..a86f6f0f 100644 --- a/app/features/src/main/res/raw/health_insurance_contacts.json +++ b/app/features/src/main/res/raw/health_insurance_contacts.json @@ -90,8 +90,8 @@ { "name": "AOK Rheinland/Hamburg - Die Gesundheitskasse", "healthCardAndPinPhone": null, - "healthCardAndPinMail": "aok@rh.aok.de", - "healthCardAndPinUrl": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/elektronische-gesundheitskarte-egk/", "pinUrl": "https://www.aok.de/pk/rh/inhalt/pin-zur-elektronischen-gesundheitskarte-egk-5/", "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", @@ -146,8 +146,8 @@ "name": "BKK Deutsche Bank AG", "healthCardAndPinPhone": null, "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkkdb.de/leistungen-beratung/alle-leistungen/alle-leistungen-von-a-z/versichertenkarte", - "pinUrl": null, + "healthCardAndPinUrl": "https://www.bkkdb.de/leistungen-beratung/alle-leistungen/alle-leistungen-von-a-z/elektronisches-rezept", + "pinUrl": "https://www.bkkdb.de/leistungen-beratung/alle-leistungen/alle-leistungen-von-a-z/elektronisches-rezept", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -157,7 +157,7 @@ "name": "BARMER", "healthCardAndPinPhone": "+498003331010", "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.barmer.de/gematik-eRezept", + "healthCardAndPinUrl": "https://www.barmer.de/unsere-leistungen/apps-skills/pin-brief-fuer-die-gesundheitskarte-1244332", "pinUrl": null, "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, @@ -219,17 +219,6 @@ "subjectPinMail": null, "bodyPinMail": null }, - { - "name": "BKK BPW Bergische Achsen KG", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, { "name": "BKK EUREGIO", "healthCardAndPinPhone": null, @@ -245,8 +234,8 @@ "name": "BKK EVM", "healthCardAndPinPhone": null, "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, + "healthCardAndPinUrl": "https://www.bkk-evm.de/", + "pinUrl": "https://www.bkk-evm.de/", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -351,17 +340,6 @@ "subjectPinMail": null, "bodyPinMail": null }, - { - "name": "BKK Herford Minden Ravensberg", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, { "name": "BKK Herkules", "healthCardAndPinPhone": "+49561208550", @@ -373,17 +351,6 @@ "subjectPinMail": null, "bodyPinMail": null }, - { - "name": "BKK HMR", - "healthCardAndPinPhone": "+4952211026210", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, { "name": "BKK KARL MAYER", "healthCardAndPinPhone": null, @@ -408,7 +375,7 @@ }, { "name": "BKK Melitta HMR", - "healthCardAndPinPhone": "+4957197590", + "healthCardAndPinPhone": "+49571934090", "healthCardAndPinMail": "info@bkk-melitta.de", "healthCardAndPinUrl": "https://www.bkk-melitta.de/", "pinUrl": null, @@ -430,10 +397,10 @@ }, { "name": "BKK Miele", - "healthCardAndPinPhone": "+498008002189", + "healthCardAndPinPhone": null, "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.miele-bkk.de/service/elektronische-gesundheitskarte", - "pinUrl": null, + "healthCardAndPinUrl": "https://www.miele-bkk.de/service/e-rezept", + "pinUrl": "https://www.miele-bkk.de/service/elektronische-gesundheitskarte/pin-puk", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -474,7 +441,7 @@ }, { "name": "BKK ProVita", - "healthCardAndPinPhone": "+498006648808", + "healthCardAndPinPhone": null, "healthCardAndPinMail": null, "healthCardAndPinUrl": "https://bkk-provita.de/service-info/e-rezept/", "pinUrl": null, diff --git a/app/features/src/main/res/raw/maps_christmas_style.json b/app/features/src/main/res/raw/maps_christmas_style.json new file mode 100644 index 00000000..5c599478 --- /dev/null +++ b/app/features/src/main/res/raw/maps_christmas_style.json @@ -0,0 +1,63 @@ +[ + { + "featureType": "water", + "stylers": [ + { "color": "#a0d8f1" } + ] + }, + { + "featureType": "landscape", + "elementType": "geometry", + "stylers": [ + { "color": "#e0eee0" } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { "color": "#ff9999" } + ] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [ + { "color": "#ffcccc" } + ] + }, + { + "featureType": "road.local", + "elementType": "geometry", + "stylers": [ + { "color": "#ffffff" } + ] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [ + { "color": "#fde293" } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { "color": "#ccdecc" } + ] + }, + { + "featureType": "transit", + "stylers": [ + { "visibility": "off" } + ] + }, + { + "featureType": "administrative", + "elementType": "labels.text.fill", + "stylers": [ + { "color": "#7492a8" } + ] + } +] diff --git a/app/features/src/main/res/raw/maps_dark_style.json b/app/features/src/main/res/raw/maps_dark_style.json new file mode 100644 index 00000000..c938c6fd --- /dev/null +++ b/app/features/src/main/res/raw/maps_dark_style.json @@ -0,0 +1,233 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#1d2c4d" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#8ec3b9" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#1a3646" + } + ] + }, + { + "featureType": "administrative.country", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#4b6878" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#64779e" + } + ] + }, + { + "featureType": "administrative.province", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#4b6878" + } + ] + }, + { + "featureType": "landscape.man_made", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#334e87" + } + ] + }, + { + "featureType": "landscape.natural", + "elementType": "geometry", + "stylers": [ + { + "color": "#023e58" + } + ] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [ + { + "color": "#283d6a" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#6f9ba5" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#1d2c4d" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry.fill", + "stylers": [ + { + "color": "#023e58" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#3C7680" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#304a7d" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#98a5be" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#1d2c4d" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#2c6675" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#255763" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#b0d5ce" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#023e58" + } + ] + }, + { + "featureType": "transit", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#98a5be" + } + ] + }, + { + "featureType": "transit", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#1d2c4d" + } + ] + }, + { + "featureType": "transit.line", + "elementType": "geometry.fill", + "stylers": [ + { + "color": "#283d6a" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [ + { + "color": "#3a4762" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#0e1626" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#4e6d70" + } + ] + } +] \ No newline at end of file diff --git a/app/features/src/main/res/raw/maps_easter_style.json b/app/features/src/main/res/raw/maps_easter_style.json new file mode 100644 index 00000000..dd95240c --- /dev/null +++ b/app/features/src/main/res/raw/maps_easter_style.json @@ -0,0 +1,69 @@ +[ + { + "featureType": "water", + "stylers": [ + { "color": "#a1daf8" } + ] + }, + { + "featureType": "landscape", + "elementType": "geometry.fill", + "stylers": [ + { "color": "#e6e6e6" } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { "color": "#fed2a6" } + ] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [ + { "color": "#ffffff" } + ] + }, + { + "featureType": "road.local", + "elementType": "geometry", + "stylers": [ + { "color": "#ffffff" } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { "color": "#cbe6a3" } + ] + }, + { + "featureType": "administrative", + "stylers": [ + { "visibility": "off" } + ] + }, + { + "featureType": "transit", + "stylers": [ + { "visibility": "off" } + ] + }, + { + "featureType": "poi", + "elementType": "labels.icon", + "stylers": [ + { "visibility": "off" } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { "color": "#9e9e9e" } + ] + } +] diff --git a/app/features/src/main/res/raw/maps_erp_style.json b/app/features/src/main/res/raw/maps_erp_style.json new file mode 100644 index 00000000..3969d525 --- /dev/null +++ b/app/features/src/main/res/raw/maps_erp_style.json @@ -0,0 +1,50 @@ +[ + { + "featureType": "water", + "stylers": [ + { "color": "#5991c2" } + ] + }, + { + "featureType": "landscape", + "elementType": "geometry", + "stylers": [ + { "color": "#f5f5f5" } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { "color": "#d5d5d5" }, + { "lightness": 50 } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { "color": "#808080" } + ] + }, + { + "featureType": "transit", + "stylers": [ + { "color": "#d5d5d5" }, + { "visibility": "off" } + ] + }, + { + "featureType": "poi.medical", + "elementType": "geometry", + "stylers": [ + { "color": "#e6a0af" } + ] + }, + { + "featureType": "poi.business", + "stylers": [ + { "visibility": "off" } + ] + } +] diff --git a/app/features/src/main/res/values-ar/strings.xml b/app/features/src/main/res/values-ar/strings.xml index af723104..4febc3b9 100644 --- a/app/features/src/main/res/values-ar/strings.xml +++ b/app/features/src/main/res/values-ar/strings.xml @@ -30,7 +30,7 @@ عدم الإلغاء دعنا نذهب ما تحتاج إليه: - أدخل رقم الوصول للبطاقة + إدخال رقم الدخول إدخال رقم التعريف الشخصي جرب مرة أخرى فشل الاتصال بالخادم. @@ -63,26 +63,23 @@ هيئة التحرير الناشر gematik GmbH\nFriedrichstraße 136\n10117 Berlin - المدير الإداري: د. فلوريان هارتج\n محكمة التسجيل: محكمة مقاطعة برلين-شارلوتنبورغ\n رقم السجل التجاري: HRB 96351\n رقم تعريف ضريبة القيمة المضافة: DE241843684 + الإدارة: د. فلوريان فورمان، برينيا أدجي، د. فلوريان هارتج\n محكمة التسجيل: محكمة مقاطعة برلين-شارلوتنبورغ\n رقم السجل التجاري: HRB 96351\n رقم تعريف ضريبة القيمة المضافة: DE241843684 المسؤول عن المحتوى - دكتور. فلوريان هارتج + دكتور. فلوريان فورمان، برينيا أدجي، د. فلوريان هارتج الاتصال ملاحظة نسعى جاهدين من أجل استخدام لغة منصفة بين الجنسين. إذا لاحظت أية أخطاء، فإننا نتطلع إلى إرسال رسالة لنا عبر البريد الإلكتروني. المنصة الألمانية الحديثة للطب الرقمي كتابة بريد إلكتروني فتح الموقع الإلكتروني - أهلًا وسهلًا - ابدأ تسجيل الدخول إلغاء الحظر يسجل إلغاء - الأمان تعليمات قانونية هيئة التحرير حماية البيانات شروط الاستخدام - التفاصيل + تفاصيل الوصفة وضع علامة \"تم الصرف\" وضع علامة \"لم يتم الصرف\" شكل الجرعة @@ -104,8 +101,6 @@ رقم المُنْشَأَة رقم الهاتف عنوان البريد الإلكتروني - حادث عمل - يوم الحادث رقم شركة التأمين على الحوادث أو صاحب العمل هل ترغب في حذف هذه الوصفة بشكل دائم؟ حذف @@ -128,7 +123,6 @@ فتح الماسح الضوئي للوصفات الطبية الإعدادات منع أخذ لقطات الشاشة - يمنع عرض الصور المصغرة للمعاينة عند التبديل بين التطبيقات هل تسمح للوصفة الطبية الإلكترونية بتحليل سلوك الاستخدام الخاص بك بشكل مجهول؟ معلومات تقنية أمان بيانات وصفاتك @@ -150,23 +144,15 @@ يظل التحليل دون إفصاح عن الهوية غير مفعل %s شكرًا لك على المساعدة! يسجل - يُرجى تحديد هويتك لتنزيل الوصفات. - ملاحظة للصيدليات: يحصل هذا التطبيق على تفاصيل الاتصال والمعلومات حول الصيدليات من الموقعmein-apothekenportal.de التابع لاتحاد الصيدليات الألماني ج.م. هل اكتشفت خطأ أو ترغب في تصحيح البيانات؟ + يرجى التعريف بنفسك. + ملاحظة للصيدليات: نحصل على تفاصيل الاتصال والمعلومات المتعلقة بالصيدليات من mein-apothekenportal.de. هل اكتشفت خطأ أو ترغب في تصحيح البيانات؟ معرفة المزيد الصيدليات فشلك المحاولة للأسف \uD83D\uDE15 من فضلك حاول مرة أخرى. - إدخال كلمة المرور - متابعة وسائل المساعدة في الاستخدام - التكبير - يتيح تكبير حجم التطبيق عبر ضم أو سحب الأصابع (الشد للتكبير). - كلمة المرور - قم بتأمين بياناتك بكلمة سر من اختيارك. - كلمة المرور + تمكين التكبير حفظ - إظهار كلمة السر - كرر كلمة المرور التوصيات:%s كتابة بريد إلكتروني أثناء إرسال رسالتك سيتم نقل المعلومات التالية عبر الجهاز ونظام التشغيل المُستخدم: @@ -177,7 +163,7 @@ الإرسال الفلتر الفرز - لا يوجد مقر متاح + مشاركة الموقع مفهوم كلمة المرور المكرر مطابقة خطأ 20 10 76631 @@ -191,8 +177,6 @@ تم اكتشاف %s محاولات تسجيل الدخول غير الناجحة. - اختر الطريقة الأكثر أمانًا - يمكن أن يكون هذا بصمة الإصبع أو نمط التمرير السريع أو ما شابه ذلك الرموز رمز الوصول رموز الدخول الموحّد (SSO) @@ -200,7 +184,7 @@ لا يتوفر الرمز الموحد المميز (SSO) تم النسخ إلى الحافظة اضغط لإضافة الرمز المميز إلى الحافظة - صالحة اليوم فقط + قابلة للاسترداد فقط اليوم السماح لا يوجد اتصال بالخادم يُرجى المحاولة من جديد بعد دقائق قليلة @@ -270,7 +254,7 @@ %s وصفات جديدة قابلة للصرف - في الخلاص + يتم تجهيزها تم الصرف غير معروف عرض بروتوكول الدخول @@ -282,7 +266,7 @@ جاري معالجة الوصفة الطبية في الوقت الحالي ولا يمكن صرفها. استعرض - قبل - قبول يبدو أن المحاولة فشلت - ونحن ندرك أن الارتباط بالبطاقة الصحية له مخاطره. في المستقبل، يجب أن يكون التسجيل ممكنًا أيضًا عبر تطبيق التأمين الصحي المعتمد بالفعل. \n\n نحن نعمل أيضًا على ضمان إمكانية صرف الوصفات الطبية رقميًا دون التسجيل. \n\n هل لاحظت أي شيء خلال هذه العملية وترغب في مشاركته معنا؟ يرجى الكتابة إلينا، وسنكون سعداء أيضًا بتلقي تعليقات انتقادية للغاية. + ونحن ندرك أن الارتباط بالبطاقة الصحية له مخاطره. في المستقبل، يجب أن يكون التسجيل ممكنًا أيضًا عبر تطبيق التأمين الصحي المعتمد بالفعل.\n\nنحن نعمل أيضًا على ضمان إمكانية استرداد الوصفات الطبية رقميًا دون التسجيل.\n\nهل لاحظت أي شيء خلال هذه العملية وترغب في مشاركته معنا؟ يرجى الكتابة إلينا، وسنكون سعداء أيضًا بتلقي تعليقات انتقادية للغاية. نصائح الاتصال حسن من قوة شبكة الاتصال انزع عند الضرورة علبة الحماية. @@ -305,7 +289,7 @@ تم المسح الضوئي في %s وضع علامة \"تم الصرف\" في %s كيف تريد المواصلة؟ - اطلب + إرسال إلى الصيدلية متوفر في القريب العاجل احجز الآن للتحصيل أو احصل عليه عن طريق خدمة البريد السريع أو الشحن الحفظ للطلب لاحقًا @@ -339,13 +323,11 @@ https://www.openstreetmap.org/copyright الخصوصية والاستخدام متابعة - لقد تلقيت رقم التعريف الشخصي لبطاقتك الصحية من شركة التأمين الصحي الخاصة بك باستخدام إجراء آمن مثل Postident. + كان عليك أن تطلب رقم التعريف الشخصي (PIN) الخاص ببطاقتك الصحية من شركة التأمين الصحي الخاصة بك ثم تستلمه عبر عملية آمنة مثل Postident. تم استلام رمز PIN رمز PIN تحقق من الاتصال بالإنترنت وإعدادات الوقت / التاريخ لجهازك. - لتسجيل الدخول، اضغط على \"فتح\". هل تم الإغلاق؟ يرجى التحقق من بيانات الدخول البيومترية الخاصة بك على هذا الجهاز. - نسيت كلمة المرور؟ يرجى حذف التطبيق ثم إعادة تثبيته. يمكنك معرفة السبب في %s نطاق المساعدة حجم العبوة والوحدة المادة الفعالة @@ -358,10 +340,6 @@ تراجع ملاحظة ساعدنا في جعل هذا التطبيق أفضل - إدخال كلمة المرور - يجب أن تحتوي كلمة المرور على ثمانية حروف على الأقل - قوة كلمة المرور غير كافية - قوة كلمة المرور كافية كلمة المرور مرئية كلمة المرور غير مرئية القياس البيومتري @@ -470,9 +448,6 @@ تم الإرسال للتو تم الإرسال الساعة %s لم يعد صالحًا - التسجيل بالتطبيق - اختر التأمين الصحي - لم تجد ما كنت تبحث عنه؟ يتم توسيع هذه القائمة باستمرار. التسجيل بالبطاقة الصحية مدعوم بالفعل من قبل كل شركة تأمين صحي. ملاحظة من تطبيق الوصفة الإلكترونية نحن سعداء بملاحظاتك. يرجى استخدام المساحة أدناه مع توخي الدقة قدر الإمكان: مفتاح فك القفل الشخصي @@ -486,13 +461,13 @@ ملاحظة لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. إعداد التأمين البيومتري - لا يمكن حفظ بيانات الدخول. قم بإعداد التأمين البيومتري (مثل بصمة الإصبع) على جهازك مسبقًا. + ليس من الممكن حفظ بيانات الوصول. قم مسبقًا بإعداد الأمان البيومتري (مثل بصمة الإصبع) على جهازك. إلغاء الإعدادات ملاحظة استعرض - قبل - قبول أمان بيانات وصفاتك - \ \"يستخدم هذا التطبيق المستشعر البيومتري الأكثر أمانًا الذي يوفره جهازك لتخزين بيانات الاعتماد الخاصة بك في منطقة آمنة من ذاكرة الجهاز. \\" + \يستخدم هذا التطبيق مستشعر القياسات الحيوية الأكثر أمانًا الذي يوفره جهازك لتأمين بيانات الاعتماد الخاصة بك في منطقة محمية من مساحة تخزين الجهاز.\ يسمح لكنظام التأمين البيومتري لبيانات الدخول الخاصة بك بفتح هذا التطبيق في المستقبل دون الحاجة إلى إدخال رقم التعريف الشخصي أو البطاقة الصحية ، وعرض الوصفات الطبية أو استدعائها أو استرداد قيمتها أو حذفها. يُرجى الانتباه إلى أن الأشخاص الذين تشارك معهم هذا الجهاز والذين قد يمكنهم تخزين الصفات البيومترية لهم على هذا الجهاز، قد يمكنهم أيضًا الوصول إلى وصفاتك الطبية. فشلت المحاولة للأسف @@ -505,7 +480,7 @@ الاسم التأمين الصحي الرقم التأميني - رقم الوصول للبطاقة + رقم الدخول يسجل تسجيل الخروج حفظ @@ -521,8 +496,7 @@ فقدت الاتصال الاتصال بخادم الوصفة الآن؟ لا الرموز - سوف تتلقى رمزًا مميزًا عند تسجيل الدخول إلى خدمة الوصفات الطبية.\n - الطلب #٪ s + تتلقى رمزًا بعد التسجيل في خدمة الوصفات الطبية. حدد رقم التعريف الشخصي المطلوب فتح البطاقة حدد رقم التعريف الشخصي @@ -539,26 +513,26 @@ الحصول على كود الاستلام لا يمكن عرض الرسالة من فضلك اتصل بالصيدلية الخاصة بك ( %s ). - إظهار رابط سلة التسوق + رابط الصيدلية عرض كود الاستلام إظهار الرسالة %s في الساعة %s - تم إرسال الوصفة إلى %s . لا تزال عملية الاسترداد الرقمي غير مألوفة للعديد من الصيدليات. إذا لم تتلقى ردًا بحلول الغد، فنوصيك بالاتصال للاستفسار كإجراء احترازي. + تم إرسال الوصفة إلى %s . بعض الصيدليات ليس لديها خيار الاستجابة الرقمية حتى الآن. إذا لم يتم الرد بحلول يوم غد، يرجى الاتصال كإجراء احترازي. نظرة عامة على الطلب جديد دورة الأمر - الطلب - مجانا للمتصل. أوقات الخدمة: الإثنين - الجمعة 8:00 صباحًا - 8:00 مساءً باستثناء أيام العطل الوطنية + مجانا للمتصل. أوقات الخدمة: الإثنين - الجمعة 8:00 صباحًا - 8:00 مساءً باستثناء أيام العطل الفيدرالية الصيدلية حدد رقم التعريف الشخصي المطلوب تم حفظ رقم التعريف الشخصي المطلوب مفتوحة حاليا وقريبة مني مصنف بواسطة … - ابدأ البحث + يطلب التكليف المباشر الصيدليات رقم الهاتف (اختياري) - البحث بالاسم أو العنوان + يطلب لا توجد معلومات صيدلية صالحة لم يتم العثور على معلومات حالية عن هذه الصيدلية. سيتم حذف الإدخال الخاص بهذه الصيدلية. موافق @@ -578,16 +552,15 @@ تحسينات المنتج تحليل مجهول ساعدنا على تحسين هذا التطبيق. يتم جمع جميع بيانات الاستخدام بشكل مجهول وتعمل حصريا على تحسين تجربة الاستخدام. - أمان التطبيق اعدادات شخصية وسائل المساعدة في الاستخدام تحسينات المنتج وصفة المضافة الوصفة متاحة بالفعل حدث خطأ أثناء الاستيراد + رقم الدخول حذف الوصفة الطبية التي تم مسحها - يمكن تلقي مستحضرًا طبيًا بديلًا نسيت رقم التعريف الشخصي @@ -607,7 +580,7 @@ يمكنك تغيير هذا القرار في أي وقت في إعدادات النظام. المواصلة استعرض - قبل - قبول - يستخدم هذا التطبيق الطريقة الأكثر أمانًا التي يوفرها جهازك. + يستخدم التطبيق الطريقة الأكثر أمانًا التي قمت بإعدادها على جهازك. حفظ يختار الدواء @@ -635,15 +608,13 @@ ما هو التكليف المباشر؟ مع الإحالة المباشرة، يتم صرف الوصفة الطبية من عيادة أو مستشفى مباشرة في الصيدلية. لا يتعين على الأشخاص المؤمن عليهم اتخاذ أي إجراء ولا يمكنهم التدخل في عملية الاسترداد. \n\n يتم إدراج الإحالات المباشرة في تطبيق الوصفات الطبية الإلكترونية لجعل علاجك أكثر شفافية بالنسبة لك. رسوم خدمة الطوارئ - في بعض الأحيان هناك حاجة للتسرع. يمكن استبدال بعض الوصفات الطبية دون دفع رسوم خدمة الطوارئ الإضافية ، مثل الليل أو في أيام العطلات. + إذا تم ملء الوصفة الطبية بين الساعة 8 مساءً و6 صباحًا أو في أيام الأحد والعطلات الرسمية، فقد يتم فرض رسوم إضافية قدرها 2.50 يورو. الأدوية الخاضعة للدفع المشترك معفاة من الدفع الإضافي - يجب على أولئك الذين لديهم تأمين صحي قانوني دفع مبلغ إضافي يصل إلى عشرة يورو مقابل الأدوية الموصوفة. \n\n يعتمد مبلغ الدفعة الإضافية على سعر الدواء الخاص بك. يتعين عليك أن تدفع بنفسك ثمن الأدوية التي تكلف أقل من 5 يورو.\n بالنسبة للأدوية الأكثر تكلفة، عليك أن تدفع عشرة بالمائة من السعر، ولكن على الأقل 5 يورو والحد الأقصى 10 يورو. \n\n يُعفى الأطفال والشباب الذين تقل أعمارهم عن 18 عامًا بشكل عام من الدفع الإضافي. \n\n إذا تجاوزت تكاليف الدواء السنوية الحد الأقصى للعبء المالي، فيمكنك إعفاءك من الدفع المشترك. تحدث إلى شركة التأمين الصحي الخاصة بك حول هذا الموضوع. + عادةً ما يدفع الأشخاص الذين لديهم تأمين صحي قانوني حدًا أقصى قدره 10 يورو مقابل الأدوية الموصوفة. قد يتم تطبيق رسوم أعلى في حالة طلب دواء من شركة تصنيع معينة لا تغطيه اتفاقية خصم التأمين الصحي (\"طلب الدواء\"). \n\n يُعفى الأطفال والشباب الذين تقل أعمارهم عن 18 عامًا من المدفوعات الإضافية. \n\n إذا تم صرف الوصفات الطبية بعد مرور أكثر من 28 يومًا من إصدارها، فيجب تحمل التكاليف بالكامل. \n\n إذا كنت تنفق الكثير من الأدوية على مدار العام، فيمكنك التقدم بطلب للحصول على إعفاء من الدفع المشترك من شركة التأمين الصحي الخاصة بك أنت معفى من دفع دفعة مشتركة لهذا الدواء. سوف تقوم شركة التأمين الصحي الخاصة بك بتغطية تكلفة الدواء. ما هي مدة صلاحية هذه الوصفة الطبية؟ خلال هذه الفترة، يمكنك استرداد الوصفة الطبية الخاصة بك في أي صيدلية بحد أقصى للدفع الإضافي قدره 10 يورو. - يمكن تلقي مستحضرًا طبيًا بديلًا - نظرًا للمتطلبات القانونية من شركة التأمين الصحي الخاصة بك، قد يتم إعطاؤك بديلاً بنفس العنصر النشط. \n\n يمكن أن تبدو الأدوية وتسميتها مختلفة، ولها أسعار ومصنعون مختلفون، ولكنها لا تزال تحتوي على نفس العنصر النشط. العنصر النشط نفسه والجرعة أمران حاسمان لتأثير الأدوية في الجسم. غالبًا ما يتلقى المرضى في الصيدلية دواءً مختلفًا عن الدواء الذي وصفه الطبيب - بشرط أن يكون الدواء مشابهًا. وقد تكون هناك أسباب علاجية واقتصادية للتغيير. الوصفة الطبية التي تم مسحها الوصفات الطبية المستوردة من نسخة مطبوعة لا يمكنها عرض المعلومات الشخصية أو الطبية لأسباب أمنية. \n\n قم بتسجيل الدخول إلى هذا التطبيق باستخدام البطاقة الصحية أو تطبيق التأمين لعرض جميع المعلومات الواردة في الوصفة الطبية. الوصفة غير صحيحة @@ -653,7 +624,7 @@ الهاتف موقع إلكتروني البريد الإلكتروني - الفرز حسب المسافة غير ممكن. + شارك موقعك للعثور على الصيدليات القريبة منك. موافق أدخل رقم التعريف الشخصي الحالي تم إدخال رقم تعريف شخصي خاطئ @@ -671,12 +642,10 @@ بطاقة صحية القياس البيومتري لم يتم تسجيل الدخول - ونحن مهتمون في رأيك. من فضلك خذ خمس دقائق لإكمال الاستبيان الخاص بنا. شكرا جزيلا لك مقدما. إشعار تحذير تم اضافة الصيدلية الى المفضلة تمت إزالة الصيدلية من المفضلة صيدلياتي - قوة كلمة المرور جيدة جدا عملية الكتابة غير ناجحة لا يمكن حفظ رقم التعريف الشخصي الإبلاغ @@ -695,7 +664,6 @@ تم الاسترداد بتاريخ %s تم الاسترداد الآن تم الاسترداد في الساعة %s ظهرًا - الطلب #٪ s تم استبدال هذه الوصفة لك كجزء من العلاج. رسوم خدمة الطوارئ لا يمكن صرف هذه الوصفة الطبية في الصيدلية ليلاً دون دفع رسوم خدمة الطوارئ الإضافية. @@ -709,7 +677,7 @@ تلقي الوصفات الطبية رقميا؟ اسحب الشاشة للأسفل للتحديث. لا توجد وصفات طبية - قم بتسجيل الدخول لتلقي الوصفات تلقائيًا أو قم بإضافة وصفة جديدة باستخدام الرمز ⊕ في الزاوية العلوية. + قم بالتسجيل لتلقي الوصفات تلقائيا. يسجل أرشيف الوصفة ربما لاحقا @@ -746,9 +714,9 @@ انقر على الشاشة لتخطي تلميح الأداة الذي يظهر. كيفية تخليص؟ كيف تريد أن تتلقى الدواء الخاص بك؟ - تخليص مباشرة - استبدال الدواء في الموقع - اطلب + إظهار الرمز + قم بفحصها في الصيدلية + إرسال إلى الصيدلية حجز أو تسليمها تم كود الجمع @@ -818,7 +786,7 @@ سيؤدي هذا إلى حذف كافة إيصالات النفقات من هذا الجهاز والخادم. استلام إيصالات التكلفة يتم أيضًا حفظ إيصالات التكلفة الخاصة بك على خادم الوصفات. - تلقى + تفعيل الإجمالي: %s %s الاختيار ينقسم @@ -867,17 +835,17 @@ مطلوب رقم التعريف الشخصي المرتبط لا يمكن استبدالها إلا غدًا كدافع ذاتي لم يتبق سوى %s من الأيام للاسترداد كدافع ذاتي - \nلا تزال قابلة للاسترداد كدافع ذاتي لمدة %s من الأيام\n - صالح لمدة %s من الأيام فقط - \nصالح لمدة %s من الأيام المتبقية\n - صالحة غدا فقط + لا تزال قابلة للاسترداد كدافع ذاتي لمدة %s من الأيام + لم يتبق سوى %s من الأيام للاسترداد + لا يزال قابلاً للاسترداد لمدة %s من الأيام + قابلة للاسترداد فقط غدا سيتم احتساب رسوم اضافية يأخذ التأمين تم نقل الوصفة (الوصفات) بنجاح. لا يمكن معالجة الوصفة. حاول مرة اخرى. قد تحتاج إلى اختيار صيدلية مختلفة. لا يمكن معالجة الوصفة. أبلغت الصيدلية عن خطأ غير معروف. إذا لزم الأمر، حاول صيدلية أخرى. تم رفض الوصفة الطبية من الصيدلية. قد تكون الوصفة الطبية غير صالحة أو قد يكون عنوان التسليم أو معلومات الاتصال الخاصة بك غير صالحة. - غير قادر على الاسترداد، يرجى التحقق من اتصالك بالإنترنت. + حاول مرة أخرى وربما اختر صيدلية أخرى. إذا استمر الخطأ، يرجى إبلاغ الدعم. تم نقل الوصفة بنجاح. ومع ذلك، أبلغت الصيدلية عن خطأ في المعالجة. يرجى الاتصال بالصيدلية. تم رفض الوصفة الطبية من الصيدلية. لقد تم بالفعل صرف الوصفة الطبية. تم رفض الوصفة الطبية من الصيدلية. تم حذف الوصفة. @@ -910,7 +878,6 @@ تفعيل الوضع التجريبي ليس الآن يبحث - مطلوب هاتف ذكي يدعم تقنية NFC تلقي إيصالات التكلفة رقميا بمجرد تفعيل إيصالات التكلفة، ستجدها هنا بعد استرداد الوصفة الطبية الخاصة بك. تفعيل @@ -918,11 +885,11 @@ بمجرد قيام الصيدلية بإيداع إيصال التكلفة، سيظهر هنا. إيصال التكلفة تلقي إيصالات التكلفة رقميا - ملاحظة: لن تتلقى بعد الآن إيصالات التكاليف الخاصة بك كنسخة مطبوعة في\n مقابل.\n + ملاحظة: لن تتمكن بعد الآن من استلام إيصالات التكلفة الخاصة بك كنسخة مطبوعة في الصيدلية. تفعيل ربما لاحقا اتفاق - وبموافقتك سوف تمتنع عن طباعته في الصيدلية\n وأرسل إيصالات التكلفة الخاصة بك رقميًا في المستقبل.\n + وبموافقتك، سوف تمتنع عن طباعتها في الصيدلية وستقدم إيصالات التكلفة الخاصة بك رقميًا في المستقبل. إلغاء موافق الخروج من الوضع التجريبي @@ -937,17 +904,17 @@ لا انترنت لا يوجد اتصال بالإنترنت. طلب غير صحيح - كانت هناك مشكلة مع هذا الطلب. نحن نعمل على حل.\n + كانت هناك مشكلة مع هذا الطلب. نحن نعمل على حل. الخادم لا يستجيب يُرجى المحاولة من جديد بعد دقائق قليلة. فشل الاتصال - لا يمكن حاليا استرجاع أي معلومات. رجاء\n حاول مرة أخرى في وقت لاحق.\n + لا يمكن حاليا استرجاع أي معلومات. الرجاء معاودة المحاولة في وقت لاحق. مرفوض - لقد رفض الخادم طلبك. ارجو تجربة ذلك\n مرة أخرى في وقت لاحق.\n + لقد رفض الخادم طلبك. الرجاء معاودة المحاولة في وقت لاحق. تحديث التطبيق - لاستخدام هذه الميزة، يرجى تحديث التطبيق الخاص بك.\n + لاستخدام هذه الميزة، يرجى تحديث التطبيق الخاص بك. فشل تسجيل الدخول - واجه الخادم مشكلة في تسجيل الدخول. يرجى الاتصال\n مرة أخرى.\n + واجه الخادم مشكلة في تسجيل الدخول. الرجاء تسجيل الدخول مرة أخرى. يسجل إنسخ الرابط الاتصال بتطبيق التأمين الصحي الخارجي. @@ -973,12 +940,289 @@ مُستَحسَن معرف الصحة الرقمية ضبط الإعدادات؟ - صفحة 404؟ يرجى تمكين فتح الروابط في الإعدادات الخاصة بك للمتابعة. فتح الإعدادات نسخة التطبيق قديمة إصدار التطبيق لديك قديم ولم يعد مدعومًا. لمواصلة استخدام الوصفة الطبية الإلكترونية، يرجى تحديث التطبيق. تحديث - يبلغ طول العلبة الخاصة بك %s رقمًا. + يتكون رقم الوصول الخاص بك من %s من الأرقام. يمكن أن يتكون رقم التعريف الشخصي الخاص بك من %s إلى %s من الأرقام. + يلتقط + ساعي + شحنة + مطلوب هاتف ذكي يدعم تقنية NFC + لم نتمكن من معالجة طلب حذف الوصفة. نحن نعمل على حل ( %s ). + حدث خطأ ما أثناء حذف الوصفة الطبية الخاصة بي، تلقيت رمز الخطأ %s . الرجاء إعلامي عندما يكون الحذف ممكنًا مرة أخرى. + متابعة + طلب غير صحيح + الإبلاغ + حذف محليا + لا انترنت + لا يمكن حذف الوصفة. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى. + جرب مرة أخرى + موافق + إعادة تسجيل الدخول + يجب عليك تسجيل الدخول لحذف الوصفة (401). + يسجل + إلغاء + الحذف مستحيل + لا يجوز لك حذف الوصفة الطبية في الوقت الحالي لأنها موجودة في الصيدلية المراد صرفها أو أنها مهمة مباشرة معلقة (403). + طلبات كثيرة جدا + لقد حاولت حذف الوصفة عدة مرات. يرجى المحاولة مرة أخرى لاحقًا (429). + حذف الوصفة + أُووبس... + "حدث خطأ غير متوقع في الخادم. يرجى المحاولة مرة أخرى لاحقًا (500)." + حماية التطبيق غير ممكنة. قم مسبقًا بإعداد الأمان البيومتري (مثل بصمة الإصبع) على جهازك. + النسخ الاحتياطي البيومتري غير ممكن لهذا الجهاز لأنه غير مدعوم من قبل جهازك. + انتباه! هاتفك ليس محميًا بشكل جيد. + يتم حفظ تفاصيل تسجيل الدخول الخاصة بك على هاتفك. لحماية الوصول، يتم استخدام طريقة تسجيل الدخول المفضلة لديك. طريقة تسجيل الدخول هذه، على سبيل المثال، نمط التمرير، ليست آمنة للغاية. يمكنك استخدام ميزة \"حفظ تفاصيل تسجيل الدخول\" على مسؤوليتك الخاصة. + إذا كنت لا تزال تستخدم وظيفة \"حفظ بيانات تسجيل الدخول\"، فستتمكن من عرض الوصفات الطبية أو الوصول إليها أو استردادها أو حذفها باستخدام تطبيق الوصفات الطبية الإلكترونية في المستقبل بدون بطاقة صحية وإدخال رقم التعريف الشخصي. + لم يتم العثور على الموقع + إرسال إلى الصيدلية + إظهار الرمز + اللغة + اللغة + ألماني + عربي + البلغارية + التشيكية + دانماركي + إنجليزي + فرنسي + اللغة العبرية + ايطالي + هولندي + تلميع + روماني + الروسية + اللغة التركية + الأوكرانية + تقصير + تم إلغاء تنشيط إيصالات التكلفة الرقمية وحذفها. + "منتدى الوصفات الطبية الإلكترونية" + لا يوجد إعداد بديل ممكن + يمكن تلقي مستحضرًا طبيًا بديلًا + إعداد البديل (Aut idem) + يلتزم الصيادلة بإعطاء الأولوية لصرف الأدوية التي أبرمت شركة التأمين الصحي للمريض اتفاقية خصم لها مع الشركات المصنعة للأدوية. لا ينطبق هذا فقط إذا استبعد الطبيب عبارة \"Aut idem\" من الوصفة الطبية، وهو ما لا ينطبق على الوصفة الطبية الخاصة بك. + لقد قرر طبيبك أنك يجب أن تتلقى الدواء الموصوف لك. لا يجوز للصيدلية إجراء عملية تبادل على أساس اتفاقية الخصم (\"Aut Idem\"). + السماح بلقطات الشاشة + إذا سمحت بلقطات الشاشة، فستظل الصفحة الأخيرة التي فتحتها مرئية في الخلفية عند تبديل التطبيقات. إذا لزم الأمر، قد تكون بياناتك الشخصية مرئية. لذلك نوصي بعدم السماح بلقطات الشاشة. + السماح + إلغاء + قم بتحويل لوحة المفاتيح إلى ملصقات أو استخدم الرموز التعبيرية لصورة ملفك الشخصي. + اختر الصورة الشخصية + كيف تريد المواصلة؟ + اختر الصور + آلة تصوير + الرموز التعبيرية + إلغاء + فتح الإعدادات + يستخدم + التقط صوره + تعديل الصورة الشخصية + تم الافتراض قبل %s دقيقة + تم قبوله في %s + قبلت للتو + تم القبول في الساعة %s ظهرًا + معرف الصحة + اختر التأمين الصحي + إذا لم يتم التسجيل باستخدام المعرف الصحي كما هو متوقع، فيرجى اتباع النصائح الواردة في مركز المساعدة الخاص بنا. + المساعدة + المساعدة + نصائح للتسجيل في تطبيق التأمين + شركة التأمين الخاصة بك هي المسؤولة عن الهوية الصحية. يرجى الاتصال بهم إذا كان لديك أي أسئلة حول التسجيل. فيما يلي بعض النصائح المجربة والمختبرة: + يرجى ملاحظة أنه اعتمادًا على التأمين الخاص بك، قد تكون هناك حاجة إلى تطبيق منفصل. يرجى مراجعة شركة التأمين الخاصة بك لمعرفة ما هو هذا. + قم بتشغيل تطبيق تسجيل النقد وقم بتسجيل الدخول هناك مرة واحدة قبل بدء التسجيل في تطبيق الوصفات الطبية الإلكترونية. + قد يؤدي التبديل بين التسجيل في HealthID والبطاقة الصحية إلى حدوث مشكلات. لذلك، يرجى أولاً تسجيل الخروج من ملفك الشخصي قبل تغيير خيار تسجيل الدخول. + إذا لم يكن التأمين الخاص بك مدرجًا في القائمة، فيمكنك بدلاً من ذلك التسجيل باستخدام بطاقتك الصحية ورقم التعريف الشخصي المقابل. + إذا لم يعيد تطبيق تسجيل النقد توجيهك مرة أخرى إلى تطبيق الوصفات الطبية الإلكترونية، فيرجى التأكد من إبلاغ شركة التأمين الخاصة بك بهذا الخطأ. + إذا تعذر الوصول إلى تطبيق التأمين، فقد يكون من المفيد الانتقال إلى إعدادات متصفح هاتفك الذكي والسماح بفتح الروابط. + إذا كان هاتفك الذكي يعمل بنظام التشغيل Android 14، فقد يكون من المفيد السماح بفتح الروابط في الإعدادات. + فتح الإعدادات + فشلت المحاولة للأسف + الرجاء معاودة المحاولة في وقت لاحق. + رسائل خطأ عند التحميل. + يرجى اختيار رمز تعبيري أو نص + حفظ + الرجاء تسجيل الدخول للمتابعة + متابعة + يتصل + اكتب بريدًا\nإلكترونيًا + إلى الصيدلية + الصيدلية + اليوم + غداً + الطريق\nهنا + حذف الوصفة الطبية وإيصال التكلفة + إذا قمت بحذف الوصفة، فسيتم أيضًا حذف إيصال التكلفة المرتبط بها. + يتم الآن عرض الرسائل من جميع الملفات الشخصية معًا + ميزات جديدة + ميزات جديدة + نحن نعمل باستمرار على ميزات جديدة. شاركنا برأيك وساعدنا في بناء تطبيق أفضل. + أوامر للجميع + ضمن الطلبات، يمكنك الآن رؤية الطلبات الخاصة بجميع الملفات الشخصية معًا وعرضها بسهولة أكبر + + لا تسديد التكاليف + كقاعدة عامة، لا يغطي التأمين الصحي تكاليف هذه الوصفة الطبية. كمريض، أنت بالتالي مسؤول عن الدفع الكامل. ما إذا كان سيتم سداد التكاليف كجزء من التأمين الإضافي أو المزايا القانونية يجب التحقق منها بشكل فردي. + لن يغطي التأمين الخاص بك أي تكاليف لهذه الوصفة الطبية. + + + "التأمين الخاص بك لن يغطي الوصفة الطبية %s " + + + + "التأمين الخاص بك لن يغطي الوصفات الطبية %s ." + + تسبب + تاريخ + حادثة + حادث عمل + مرض صناعي + أخبار + لا أخبار + ليس لديك أية رسائل حتى الآن. + 🎉 طلبك جاهز للاستلام. يرجى إظهار رمز الالتقاط هذا لتعريف هويتك. + كانت الرسالة الواردة من الصيدلية للأسف فارغة. يُرجى الاتصال بالصيدلية الخاصة بك. + الصيدلية زودتك برابط . + أخبار + طلب + رسائل + لا يوجد اتصال بالإنترنت + لعرض الأجهزة المتصلة، يجب أن تكون متصلاً بالقياسات الحيوية. + تم حذف الوصفة الطبية في %s + تم الحذف + لا توجد وصفات طبية + ليس لديك أي وصفات في الأرشيف. + لقد تم حذف إيصالات النفقات الخاصة بك + إيصال التكلفة + التسجيل مطلوب + الرجاء تسجيل الدخول لعرض الأجهزة المتصلة. + يسجل + يسجل + الاتصال بالتأمين %s + البطاقة الصحية على الهاتف الذكي + لم يتم تسجيل الدخول + مسجل + ينعش + تتلقى بروتوكل الدخول بعد التسجيل في خدمة الوصفات الطبية. + التسجيل مطلوب + يرجى تسجيل الدخول باستخدام بطاقتك الصحية وحفظ تفاصيل تسجيل الدخول الخاصة بك مع القياسات الحيوية لعرض الأجهزة المتصلة. + %1$s × صباح + %1$s × الغداء + %1$s × مساء + %1$s x في الليل + ما لم يعطك طبيبك تعليمات مختلفة، يمكن فهم تعليمات الاستخدام على النحو التالي: + لقد أعطاك طبيبك هذه المعلومات حول تناول الدواء. + لا توجد معلومات حول كيفية تناول الدواء في الوصفة الطبية الخاصة بك. + لاحظ طبيبك أنه قد تم إعطاؤك تعليمات حول كيفية تناول هذا الدواء غير الموجود في الوصفة الطبية. يمكن أن يكون هذا مدرجًا في خطة الدواء الخاصة بك، على سبيل المثال. + بدون بيان + دي جي + تعليمات الاستخدام + " %s - %s" + تذكير بالإيرادات + خذ تذكيرات + الأدوية عن طريق الفم + حول تذكير المدخول + صباح + منتصف النهار + بعد الظهر + في المساء + الدواء + معلومات الجرعة + 1 + %s / %s + 1 جرعة + تغيير الجرعة + إلغاء + تذكير الدواء + جدول الأدوية + تناول الجرعة حسب وصفة الطبيب + في الساعة %s ، خذ %s x + حدد التواريخ + متابعة + يلغي + أ + خارج + المتابعة + غير محدود + بشكل فردي + اليوم الأول + اليوم الأخير + أضف الوقت + لا تذكير المدخول + يمكنك تعيين تذكيرات للوصفات الطبية الخاصة بك + على + إيقاف + تذكرنى + قم بإيقاف تشغيل تحسين البطارية لهذا التطبيق. + إذا لم تقم بتنشيط هذا الخيار، فقد تبدو تذكيرات الدواء غير موثوقة. + إلغاء + السماح + تعليمات الاستخدام + بدون بيان + معلومة + تذكر أيضًا في وضع تحسين البطارية + تغيير الجرعة + الكمية + استمارة + انتهى + غير محدود + إلى %s + كرر العملية %s + حيثما أمكن، نستخدم المعلومات المخزنة في الوصفة الطبية لإجراء الحساب. + إلغاء + حفظ + حتى نهاية الحزمة + وقت + جرعة + عرض الأدوية + هل تناولت أدويتك بالفعل؟ + خذ تذكيرات + لا توجد ذكريات نشطة + ليس لديك أي تذكيرات لتأخذها اليوم + حدد تاريخ البدء + حدد تاريخ الانتهاء + مرحباً + في تطبيق الوصفة الطبية الإلكترونية الخاص بك + حاول ثانية + أدخل كلمة المرور + يرجى التأكد من أن أي شخص قد تشارك معه هذا الجهاز ويعرف معلومات تسجيل الدخول الخاصة بك يمكنه أيضًا الوصول إلى وصفاتك. + أدخل كلمة المرور + الرجاء إدخال كلمة المرور لفتح التطبيق. + كلمة المرور + كلمة المرور + الرجاء إدخال كلمة المرور لتأمين التطبيق. + إظهار كلمة المرور + كرر كلمة المرور + نسيت كلمة السر؟ يرجى حذف التطبيق ثم إعادة تثبيته. يمكنك معرفة السبب في %s لدينا. + أدخل كلمة المرور + يجب أن تتكون كلمة المرور من ثمانية أحرف على الأقل + قوة كلمة المرور غير كافية + قوة كلمة المرور كافية + النسخ الاحتياطي للجهاز غير ممكن + يرجى إعداد نسخة احتياطية للجهاز أولاً. + يلغي + إعدادات + قوة كلمة المرور جيدة جدًا + أمان التطبيق + النسخ الاحتياطي للجهاز + يرجى اختيار طريقة واحدة على الأقل لعمل نسخة احتياطية من التطبيق. + كلمة المرور + تغيير كلمة المرور + للحذف، يجب إنشاء اتصال بخادم الوصفة الطبية + البضائع جاهزة + البضائع جاهزة منذ %s + لقد كانت البضائع جاهزة للتو + كانت البضائع جاهزة لمدة %s دقيقة + البضائع جاهزة منذ %s + معطل لأنه لم يتم إدخال كلمة المرور. + تم التعطيل لأن كلمة المرور ضعيفة جدًا. + تم التعطيل لأن كلمات المرور غير متطابقة. + يستكشف + سجل التبرع بالأعضاء + فتح سجل التبرع بالأعضاء؟ + ستتم إعادة توجيهك إلى سجل المتبرعين بالأعضاء. لكي تتمكن من عرض وتغيير بيانات التبرع بالأعضاء، يجب عليك تسجيل الدخول هناك. + يفتح + يلغي + الوظيفة غير نشطة في الوضع التجريبي diff --git a/app/features/src/main/res/values-bg/strings.xml b/app/features/src/main/res/values-bg/strings.xml index b4043952..fd4f8291 100644 --- a/app/features/src/main/res/values-bg/strings.xml +++ b/app/features/src/main/res/values-bg/strings.xml @@ -26,7 +26,7 @@ Не отменяй Да тръгваме От какво имаш нужда: - Въведете номера за достъп до картата + Въведете номер за достъп въведете ПИН кода Опитай пак Неуспешно свързване със сървъра. @@ -55,26 +55,23 @@ отпечатък редактор gematik GmbH\n Фридрихщрасе 136\n 10117 Берлин - Управляващ директор: Dr. Флориан Хартдж\n Регистрационен съд: Окръжен съд Берлин-Шарлотенбург\n Номер в търговския регистър: HRB 96351\n Идентификационен номер по ДДС: DE241843684 + Ръководство: Dr. Флориан Фурман, Бреня Аджей, д-р. Florian Hartge\nРегистрационен съд: Окръжен съд на Берлин-Шарлотенбург\nНомер на търговския регистър: HRB 96351\nИдентификационен номер по ДДС: DE241843684 Отговаря за съдържанието - д-р Флориан Хартдж + д-р Флориан Фурман, Бреня Аджей, д-р. Флориан Хартдж Контакт Забележете Стремим се да използваме език, съобразен с пола. Ако забележите грешки, ще се радваме да ни изпратите имейл. Модерната германска платформа за цифрова медицина Пишете имейл Отворете уебсайта - Добре дошли - Започнете регистрация Отключи Регистрирам Отказ - Сигурност Законни отпечатък защита на данни Условия за ползване - Подробности + Подробности за рецептата Маркирайте като изкупени Маркирайте като неизкупени Доза от @@ -96,8 +93,6 @@ Номер на завода Телефонен номер Имейл адрес - Трудова злополука - Ден на инцидента Номер на компания или работодател при злополука Искате ли да изтриете тази рецепта за постоянно? Изтрий @@ -120,7 +115,6 @@ Отворете скенер за рецепти Настройки Потискане на екранни снимки - Предотвратява показването на изображение за визуализация при превключване на приложения Позволявате ли на E-Prescription да анализира анонимно вашето поведение при използване? Техническа информация Сигурност на вашите данни за рецепта @@ -142,23 +136,15 @@ Анонимният анализ остава деактивиран %s Благодарим ви за подкрепата! Регистрирам - Моля, идентифицирайте се, за да изтеглите рецепти. - Бележка за аптеките: Получаваме данните за контакт и информация за аптеките от mein-apothekenportal.de на Германската фармацевтична асоциация. Открили ли сте грешка или искате да коригирате данните? + Моля, идентифицирайте се. + Забележка за аптеките: Получаваме данните за контакт и информацията за аптеките от mein-apothekenportal.de. Открихте ли грешка или искате да коригирате данните? Научете повече Аптеки За съжаление това не проработи \uD83D\uDE15 Моля, опитайте отново. - Въведете паролата - По-нататък Достъпност - мащабиране - Позволява ви да увеличите приложението чрез щипка за мащабиране. - парола - Защитете данните си с парола по ваш избор. - парола + Разрешете мащабирането Запазване - Покажи парола - Повтори паролата Препоръки: %s Пишете имейл Когато изпратите вашето съобщение, се предава следната информация за използвания хардуер и операционна система: @@ -169,7 +155,7 @@ Пратка филтър Филтър - Няма налично местоположение + Споделяне на местоположение Разбрах Повтарящи се пароли Грешка 20 10 76631 @@ -179,8 +165,6 @@ Бяха открити %s неуспешни опита за влизане. Открити са %s неуспешни опита за влизане. - Изберете най-доброто архивиране на устройството - Това може да бъде пръстов отпечатък, модел на плъзгане или нещо подобно Токени Токени за достъп SSO токени @@ -188,7 +172,7 @@ няма наличен SSO токен копирани в клипборда Щракнете, за да копирате токена в клипборда - Важи само днес + Може да се изкупи само днес Позволява няма връзка със сървъра Моля опитайте отново след няколко минути @@ -254,7 +238,7 @@ %s нови рецепти Може да се изкупи - В изкупление + Обработва се Изкупен неизвестен Преглед на регистрационните файлове за достъп @@ -266,7 +250,7 @@ Рецептата в момента се изпълнява и не може да бъде изтрита Приеми Явно това не проработи - Наясно сме, че връзката със здравната карта има своите подводни камъни. В бъдеще регистрацията трябва да е възможна и чрез вече удостоверено приложение за здравно осигуряване. \n\n Ние също така работим върху това да гарантираме, че рецептите могат да бъдат изкупени цифрово без регистрация. \n\n Забелязахте ли нещо по време на този процес, което бихте искали да споделите с нас? Моля, пишете ни, ние също ще се радваме да получим много критична обратна връзка. + Наясно сме, че връзката със здравната карта има своите подводни камъни. В бъдеще регистрацията трябва да е възможна и чрез вече удостоверено приложение за здравно осигуряване.\n\nНие също така работим, за да гарантираме, че рецептите могат да се изкупуват цифрово без регистрация.\n\nЗабелязахте ли нещо по време на този процес, което бихте искали да споделите с нас? Моля, пишете ни, ние също ще се радваме да получим много критична обратна връзка. Съвети за свързване Подобрете силата на връзката Ако е необходимо, отстранете защитния капак. @@ -289,7 +273,7 @@ Сканирано на %s Маркирано като осребрено на %s Как искате да продължите? - Поръчка + Изпратете в аптеката Наличен скоро Резервирайте сега за вземане или го доставете чрез куриер или доставка Запазете за последваща поръчка @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Защита на данните и използване По-нататък - Получихте ПИН кода за вашата здравна карта от вашата здравноосигурителна компания чрез сигурна процедура като Postident. + Трябваше активно да поръчате ПИН кода на вашата здравна карта от вашата здравноосигурителна компания и след това да го получите чрез защитен процес като Postident. Не е получен PIN ПИН код Проверете интернет връзката на вашето устройство и настройките за час/дата. - За да влезете, натиснете „Отключи“. Заключен? Моля, проверете биометричните си данни на това устройство. - Забравена парола? Моля, изтрийте приложението и го инсталирайте отново. Можете да разберете защо в нашия %s . зона за помощ Размер на опаковката и единица активна съставка @@ -338,10 +320,6 @@ Отменено Забележете Помогнете ни да направим това приложение по-добро - Въведете паролата - Паролата трябва да е с дължина поне осем знака - Силата на паролата не е достатъчна - Достатъчна сила на паролата Паролата се вижда Паролата не се вижда биометрия @@ -446,9 +424,6 @@ Изпратено току що Изпратено в %s време Вече не е валиден - Регистрирайте се с приложението - Изберете застраховка - Не намерихте това, което търсихте? Този списък непрекъснато се разширява. Регистрацията със здравна карта вече се поддържа от всяка здравноосигурителна компания. Обратна връзка от приложението за електронна рецепта Очакваме вашите отзиви. Моля, използвайте следното място и бъдете възможно най-точни: PUK @@ -468,7 +443,7 @@ Забележете Приеми Сигурност на вашите данни за рецепта - \"Това приложение използва най-сигурния биометричен сензор, предоставен от вашето устройство, за да защити вашите идентификационни данни в защитена зона на хранилището на устройството.\" + \Това приложение използва най-сигурния биометричен сензор, предоставен от вашето устройство, за да защити вашите идентификационни данни в защитена зона на хранилището на устройството.\ Биометричната сигурност на вашите данни за достъп ви позволява да отваряте това приложение в бъдеще, да преглеждате, извличате, осребрявате или изтривате рецепти без здравна карта и въвеждане на своя ПИН. Моля, уверете се, че хората, с които можете да споделяте това устройство и чиито биометрични характеристики може да се съхраняват на това устройство, също имат достъп до вашите рецепти. това за съжаление не проработи @@ -481,7 +456,7 @@ Фамилия Застраховка Осигурителен номер - Номер за достъп до картата + Номер за достъп Регистрирам Излез от профила си Запазване @@ -497,8 +472,7 @@ връзката е изгубена Свързване към сървъра за рецепти сега? Без жетони - Ще получите токен, когато влезете в услугата за рецепти.\n - Поръчки + Ще получите токен, когато влезете в услугата за рецепти. Изберете желания PIN Отключи картата Изберете PIN @@ -515,26 +489,26 @@ Кодът за получаване е получен Съобщението не може да се покаже Моля, свържете се с вашата аптека ( %s ). - Показване на връзката към пазарската количка + Линк към аптека Показване на кода за получаване Покажете съобщението %s в %s часа - Рецептата е изпратена до %s . Процесът на цифрово обратно изкупуване все още е непознат за много аптеки. Ако не получите отговор до утре, препоръчваме да се обадите, за да попитате като предпазна мярка. + Рецептата е изпратена до %s . Някои аптеки все още нямат опция за дигитален отговор. Ако до утре няма отговор, моля, обадете се като предпазна мярка. Преглед на поръчката Нов курс Поръчката - Безплатно за обаждащия се. Работно време: понеделник - петък 8:00 - 20:00 с изключение на национални празници + Безплатно за обаждащия се. Работно време: понеделник - петък 8:00 - 20:00 с изключение на федерални празници Аптека Изберете желания PIN Желаният ПИН е запазен В момента отворен и близо до мен Филтриране по... - започнете търсене + Търсене Директно възлагане Аптеки Телефонен номер (по избор) - Търсене по име или адрес + Търсене Няма валидна информация за аптеката Няма открита актуална информация за тази аптека. Записът за тази аптека ще бъде изтрит. Добре @@ -554,16 +528,15 @@ Подобрения на продукта Анонимен анализ Помогнете ни да направим това приложение по-добро. Всички данни за използването се събират анонимно и се използват изключително за подобряване на потребителското изживяване. - Сигурност на приложението лични настройки Достъпност Подобрения на продукта Добавена рецепта Рецептата вече е налична Възникна грешка при импортирането + Номер за достъп Изтрий Сканирана рецепта - Възможна подготовка за смяна Забравен ПИН %s Рецепта @@ -579,7 +552,7 @@ Можете да промените това решение по всяко време в системните настройки. продължи Приеми - Това приложение използва най-безопасния метод, предоставен от вашето устройство. + Приложението използва най-безопасния метод, който сте задали на вашето устройство. Запазване Избирам лекарство @@ -607,15 +580,13 @@ Какво е директно възлагане? При директно насочване рецепта от практика или болница се изпълнява директно в аптека. Застрахованите лица не трябва да предприемат никакви действия и не могат да се намесват в процеса на обратно изкупуване. \n\n Директните препоръки са изброени в приложението за електронна рецепта, за да направи лечението ви по-прозрачно за вас. Такса за спешни услуги - Понякога бързането е необходимо. Някои рецепти могат да бъдат изпълнени без допълнително заплащане на такса за спешна помощ, например през нощта или по празници. + Ако рецептата е попълнена между 20:00 и 6:00 часа или в неделя и на официални празници, може да бъде таксувана допълнителна такса от 2,50 евро. Лекарствата се доплащат Освободен от допълнително заплащане - Задължително здравноосигурените трябва да доплащат до десет евро за лекарства с рецепта. \n\n Размерът на допълнителното плащане зависи от цената на вашето лекарство. Вие сами трябва да плащате за лекарства, които струват по-малко от 5 евро.\n За лекарства, които са по-скъпи, трябва да платите десет процента от цената, но най-малко 5 евро и максимум 10 евро. \n\n Децата и младежите под 18-годишна възраст обикновено са освободени от допълнително заплащане. \n\n Ако годишните ви разходи за лекарства надхвърлят лимита ви за финансова тежест, можете да бъдете освободени от доплащане. Говорете с вашата здравноосигурителна компания за това. + Хората със законово здравно осигуряване обикновено плащат максимум 10 евро за лекарства с рецепта. Може да се прилагат по-високи такси, ако се поиска лекарство от конкретен производител, което не е покрито от споразумение за отстъпка за здравно осигуряване („лекарство за заявка“). \n\n Деца и младежи под 18 години са освободени от допълнителни плащания. \n\n Ако рецептите се изкупуват по-късно от 28 дни след издаването им, разходите се поемат изцяло. \n\n Ако харчите много лекарства през годината, можете да кандидатствате за освобождаване от доплащане от вашата здравноосигурителна компания Вие сте освободени от плащането на доплащане за това лекарство. Вашата здравноосигурителна компания ще покрие разходите за лекарството. Колко време е валидна тази рецепта? През този период можете да осребрите своята рецепта във всяка аптека с максимално допълнително плащане от 10 евро. - Възможна подготовка за смяна - Поради законови изисквания на вашата здравноосигурителна компания може да ви бъде дадена алтернатива със същата активна съставка. \n\n Лекарствата могат да изглеждат и да се наричат ​​различно, да имат различни цени и производители, но все пак да съдържат една и съща активна съставка. Самата активна съставка и дозировката са от решаващо значение за ефекта на лекарствата в организма. Често пациентите получават в аптеката различно лекарство от предписаното от лекаря – при условие че лекарството е сравнимо. Може да има терапевтични и икономически причини за промяната. Сканирана рецепта Рецептите, импортирани от хартиено копие, не могат да показват лична или медицинска информация от съображения за сигурност. \n\n Влезте в това приложение със здравна карта или застрахователно приложение, за да видите цялата информация, съдържаща се в рецептата. Рецептата е неправилна @@ -625,7 +596,7 @@ телефон уебсайт поща - Сортирането по разстояние не е възможно. + Споделете местоположението си, за да намерите аптеки близо до вас. Добре Въведете текущия ПИН Въведен грешен ПИН код @@ -643,12 +614,10 @@ Здравна карта биометрия Не сте влезли в системата - Интересуваме се от вашето мнение. Моля, отделете пет минути, за да попълните нашата анкета. Благодаря много предварително. Предупредителна бележка Аптеката е добавена към любими Аптеката е премахната от любимите Моите аптеки - Силата на паролата е много добра Операцията за запис не е успешна ПИН не можа да бъде запазен Докладвай @@ -667,7 +636,6 @@ Осребрено на %s Изкупено току-що Осребрено в %s часа - Поръчки Тази рецепта е изписана като част от лечение за вас. Такса за спешни услуги Тази рецепта не може да бъде изпълнена в аптека през нощта без допълнително заплащане на такса за спешна помощ. @@ -681,7 +649,7 @@ Получаване на рецепти дигитално? Издърпайте надолу екрана, за да опресните. Без рецепти - Влезте, за да получавате рецепти автоматично или добавете нова рецепта, като използвате ⊕ в горния ъгъл. + Регистрирайте се, за да получавате рецепти автоматично. Регистрирам Архив с рецепти Може би по-късно @@ -718,9 +686,9 @@ Кликнете върху дисплея, за да пропуснете подсказката, която се появява. Как да откупите? Как бихте искали да получите вашето лекарство? - Осребрете директно - Купете лекарства на място - Поръчка + Показване на кода + Дайте го на сканиране в аптеката + Изпратете в аптеката Резервирайте или го доставете Готов Код на колекцията @@ -786,7 +754,7 @@ Това ще изтрие всички разписки от това устройство и сървъра. Получавайте разписки за разходите Вашите разписки за разходи също се записват на сървъра за рецепти. - получено + Активирате Общо: %s %s Избирам Сплит @@ -835,17 +803,17 @@ Изисква се свързан ПИН код Може да се изкупи само утре като самоплащащ Остават само %s дни за осребряване като самоплащащ - \nВсе още може да се използва като самоплащащ за %s дни\n - Валиден само за %s дни - \nВалиден още %s дни\n - Важи само утре + Все още може да се използва като самоплащащ за %s дни + Остават само %s дни за осребряване + Все още може да се използва за %s дни + Може да се използва само утре Прилагат се такси Поема застраховка Рецептата(ите) са прехвърлени успешно. Рецептата не може да бъде обработена. Моля, опитайте отново. Може да се наложи да изберете друга аптека. Рецептата не може да бъде обработена. Аптеката съобщава за неизвестна грешка. Ако трябва, опитайте в друга аптека. Рецептата е отхвърлена от аптеката. Рецептата може да е невалидна или вашият адрес за доставка или информация за контакт може да са невалидни. - Не можете да осребрите, моля, проверете връзката си с интернет. + Опитайте отново и евентуално изберете друга аптека. Ако грешката продължава, моля, свържете се с отдела за поддръжка. Рецептата беше прехвърлена успешно. Аптеката обаче съобщава за грешка при обработката. Моля, свържете се с аптеката. Рецептата е отхвърлена от аптеката. Рецептата вече е изкупена. Рецептата е отхвърлена от аптеката. Рецептата е изтрита. @@ -878,7 +846,6 @@ Активиране на демо режим Не сега Търсене - Необходим е смартфон с NFC Получавайте цифрови разписки за разходи След като разписките за разходи бъдат активирани, ще ги намерите тук, след като осребрите вашата рецепта. Активирате @@ -886,11 +853,11 @@ Веднага след като аптеката депозира разходната бележка, тя ще се появи тук. Разходна бележка Получавайте цифрови разписки за разходи - Забележка: Вече няма да получавате разписките си за разходи като разпечатка в\n Аптека.\n + Забележка: Вече няма да получавате квитанции за разходите си като разпечатка в аптеката. Активирате Може би по-късно споразумение - С ваше съгласие ще се въздържате от разпечатването му в аптеката\n и изпращайте разписките си за разходи цифрово в бъдеще.\n + С ваше съгласие ще се въздържате от разпечатването им в аптеката и ще изпращате разходните си бележки в цифров вид в бъдеще. Отказ Съгласен Излезте от демонстрационния режим @@ -905,17 +872,17 @@ Няма интернет Няма връзка с интернет. Неправилна заявка - Имаше проблем със заявката. Работим върху решение.\n + Имаше проблем със заявката. Работим върху решение. сървърът не отговаря Моля опитайте отново след няколко минути. Неуспех при свързване - В момента не може да бъде извлечена информация. Моля те\n Опитайте отново по-късно.\n + В момента не може да бъде извлечена информация. Моля, опитайте отново по-късно. Отхвърлено - Сървърът отхвърли вашата заявка. Моля, опитайте го\n отново по-късно.\n + Сървърът отхвърли вашата заявка. Моля, опитайте отново по-късно. Актуализирайте приложението - За да използвате тази функция, моля, актуализирайте приложението си.\n + За да използвате тази функция, моля, актуализирайте приложението си. Неуспешно влизане - Сървърът имаше проблем с влизането. Моля, свържете се с нас\n отново.\n + Сървърът имаше проблем с влизането. Моля, влезте отново. Регистрирам Копиране на URL Свържете се с външно приложение за здравно осигуряване. @@ -941,12 +908,285 @@ Препоръчва се Цифров здравен ID Коригиране на настройките? - 404 страница? Моля, активирайте отварянето на връзки в настройките си, за да продължите. Отворете Настройки Остаряла версия на приложението Вашата версия на приложението е остаряла и вече не се поддържа. За да продължите да използвате електронната рецепта, моля, актуализирайте приложението. Да се ​​актуализира - Вашият CAN е дълъг %s цифри. + Вашият номер за достъп е дълъг %s цифри. Вашият ПИН код може да съдържа %s до %s цифри. + Вдигни + Куриер + Пратка + Необходим е смартфон с NFC + Не успяхме да обработим заявката за изтриване на рецептата. Работим върху решение ( %s ). + Нещо се обърка при изтриването на моята рецепта, получих код за грешка %s . Моля, уведомете ме, когато изтриването е възможно отново. + По-нататък + Неправилна заявка + Докладвай + Изтрий локално + Няма интернет + Рецептата не можа да бъде изтрита. Моля, проверете вашата интернет връзка и опитайте отново. + Опитай пак + Добре + Влезте отново + Трябва да влезете, за да изтриете рецептата (401). + Регистрирам + Отказ + Изтриването е невъзможно + В момента нямате право да изтриете рецептата, защото е в аптеката за изпълнение или е чакащо директно възлагане (403). + Твърде много заявки + Опитвали сте да изтриете рецептата твърде много пъти. Моля, опитайте отново по-късно (429). + Изтриване на рецепта + опа... + „Възникна неочаквана грешка в сървъра. Моля, опитайте отново по-късно (500).“ + Сигурността на приложението не е възможна. Предварително настройте биометрична защита (напр. пръстов отпечатък) на вашето устройство. + Биометрично архивиране не е възможно за това устройство, тъй като не се поддържа от вашето устройство. + опасност! Вашият телефон не е добре защитен. + Вашите данни за вход се запазват на вашия телефон. За защита на достъпа се използва предпочитаният от вас метод за влизане. Този метод за влизане, например модел на плъзгане, не е много сигурен. Използвате функцията „Запазване на данните за вход“ на свой собствен риск. + Ако все още използвате функцията „Запазване на данните за вход“, в бъдеще ще можете да преглеждате, осъществявате достъп, осребрявате или изтривате рецепти с приложението за електронна рецепта без здравна карта и въвеждане на ПИН. + Местоположението не е намерено + Изпратете в аптеката + Показване на код + език + език + Немски + Aрабски + български + Чешки + Датски + Английски + Френски + иврит + Италиански + холандски + Полски + Pумънски + Руски + Турски + Украински + по подразбиране + Цифровите разписки за разходи са деактивирани и изтрити. + „Форум за електронни рецепти“ + Не е възможен заместител на препарата + Възможна подготовка за смяна + Подготовка за заместител (Aut idem) + Фармацевтите са длъжни да отпускат с предимство лекарства, за които здравната каса на пациента е сключила договор за отстъпка с производителите на лекарства. Това не важи само ако лекарят изключи „Aut idem“ от рецептата, което не е случаят с вашата рецепта. + Вашият лекар е преценил, че трябва да получавате предписаното лекарство. Аптеката не трябва да извършва обмен въз основа на договор за отстъпка („Aut idem“). + Разрешаване на екранни снимки + Ако разрешите екранни снимки, последната отворена страница ще остане видима на заден план, когато превключвате между приложенията. При необходимост вашите лични данни могат да бъдат видими. Затова препоръчваме да не разрешавате екранни снимки. + Позволява + Отказ + Превключете клавиатурата си към стикери или използвайте емотикони за профилната си снимка. + Изберете профилна снимка + Как искате да продължите? + Изберете снимка + камера + Емотикони + Отказ + Отворете Настройки + Използвайте + направи снимка + Редактиране на профилна снимка + Предполага се преди %s минути + Прието на %s + Току-що прието + Прието в %s часа + HealthID + Изберете застраховка + Ако регистрацията със Health ID не върви според очакванията, моля, следвайте съветите от нашия помощен център. + Помогне + Помогне + Съвети за регистрация в застрахователното приложение + Вашата застрахователна компания е отговорна за Health ID. Моля, свържете се с тях, ако имате въпроси относно регистрацията. Ето няколко изпитани съвета: + Моля, обърнете внимание, че в зависимост от вашата застраховка може да е необходимо отделно приложение. Моля, консултирайте се с вашата застрахователна компания, за да разберете какво е това. + Стартирайте приложението за касов апарат и влезте там веднъж, преди да започнете да се регистрирате в приложението за електронна рецепта. + Превключването между влизане със Health ID и здравната карта може да доведе до проблеми. Ето защо, моля, излезте активно от вашия профил, преди да промените опцията за влизане. + Ако вашата застраховка не е в списъка, можете алтернативно да се регистрирате със здравната си карта и съответния ЕГН. + Ако приложението за касов апарат не ви пренасочи обратно към приложението за електронна рецепта, моля, уведомете за тази грешка вашата застрахователна компания. + Ако приложението за застраховка не може да бъде достъпно, може да е полезно да отидете в настройките на браузъра на вашия смартфон и да разрешите отварянето на връзки. + Ако вашият смартфон работи с Android 14, може да е полезно да разрешите отварянето на връзки в настройките. + Отворете Настройки + това за съжаление не проработи + Моля, опитайте отново по-късно. + Съобщения за грешка при зареждане. + Моля, изберете емотикони или текст + Запазване + Моля, влезте, за да продължите + По-нататък + Обадете се + Пишете\nпоща + До аптеката + Аптека + Днес + Утре + Маршрут\nтук + Изтриване на рецепта и касова бележка + Ако изтриете рецептата, свързаният разход също се изтрива. + Съобщенията от всички профили вече се показват заедно + Нови функции + Нови функции + Постоянно работим върху нови функции. Споделете вашето мнение с нас и ни помогнете да създадем по-добро приложение. + Поръчки за всеки + Под Поръчки вече можете да видите поръчките за всички профили заедно и да ги разглеждате много по-лесно + + Без възстановяване на разходите + По правило здравната застраховка не покрива разходите за тази рецепта. Следователно като пациент вие носите отговорност за пълното плащане. Дали разходите ще бъдат възстановени като част от допълнителна застраховка или законови обезщетения трябва да се провери индивидуално. + Вашата застраховка няма да покрие никакви разходи за тази рецепта. + + „Вашата застраховка няма да покрие рецептата %s “ + „Вашата застраховка няма да покрие рецептите %s .“ + + Причинени + Дата + Злополука + Трудова злополука + Индустриална болест + Новини + Няма новини + Все още нямате никакви съобщения. + 🎉 Вашата поръчка е готова за получаване. Моля, покажете този код за получаване, за да се идентифицирате. + За съжаление съобщението от вашата аптека беше празно. Моля, свържете се с вашата аптека. + Аптеката ви е предоставила линк. + Новини + ред + Съобщения + Няма интернет връзка + За да видите свързаните устройства, трябва да сте свързани към биометричните данни. + Рецептата беше изтрита на %s + Изтрито + Без рецепти + Нямате рецепти в архива. + Разходните ви бележки са изтрити + Разходна бележка + Изисква се регистрация + Моля, влезте, за да видите свързаните устройства. + Регистрирам + Регистрирам + Свържете се с %s застраховка + Здравна карта на смартфон + Не сте влезли в системата + Регистриран + опресняване + Ще получите регистрационни файлове за достъп, ако сте влезли в услугата за рецепти. + Изисква се регистрация + Моля, влезте със здравната си карта и запазете данните си за вход с биометрични данни, за да видите свързаните устройства. + %1$s x сутрин + %1$s x обяд + %1$s x Вечер + %1$s x През нощта + Освен ако Вашият лекар не Ви е дал други инструкции, инструкциите за употреба могат да се разбират, както следва: + Вашият лекар Ви е дал тази информация относно приема на лекарството. + Във вашата рецепта няма информация за това как да приемате лекарството. + Вашият лекар е забелязал, че сте получили инструкции как да приемате това лекарство, което не е от рецепта. Това може да е във вашия план за лечение, например. + Неопределено + DJ + Указания за употреба + " %s - %s" + Напомняне за приходи + Приемайте напомняния + Перорални лекарства + Относно напомнянията за прием + сутрин + Обяд + Следобед + Вечерта + лекарство + Информация за дозировката + 1 + %s / %s + 1 доза + Промяна на дозировката + Отказ + Напомняне за лекарства + Схема на прием на лекарства + Приемайте дозата според лекарското предписание + В %s часа вземете %s x + Изберете дати + По-нататък + Отказ + А + Извън + Проследяване + Неограничен + Индивидуално + първи ден + Последен ден + Добавете време + Без напомняния за прием + Можете да зададете напомняния за вашите рецепти + Включено + изключено + запомни ме + Изключете оптимизирането на батерията за това приложение. + Ако не активирате тази опция, напомнянията за лекарства може да изглеждат ненадеждни. + Отказ + Разрешете + Указания за употреба + Неопределено + информация + Също така запомнете в режим на оптимизиране на батерията + Промяна на дозировката + тълпа + форма + приключи + неограничен + до %s + Повторете %s + Когато е възможно, използваме информацията, съхранена в рецептата, за изчислението. + Отказ + Запазване + До края на опаковката + време + доза + Преглед на лекарствата + Взе ли си вече лекарствата? + Приемайте напомняния + Няма активни спомени + Нямате напомняния за днес + Изберете начална дата + Изберете крайна дата + Добре дошли + във вашето приложение за електронна рецепта + Опитайте отново + Въведете парола + Моля, уверете се, че всеки, с когото можете да споделите това устройство и който знае информацията ви за вход, също има достъп до вашите рецепти. + Въведете парола + Моля, въведете паролата, за да отключите приложението. + парола + парола + Моля, въведете парола, за да защитите приложението. + Покажи парола + Повторете паролата + Забравена парола? Моля, изтрийте приложението и го инсталирайте отново. Можете да разберете защо в нашия %s. + Въведете парола + Паролата трябва да е с дължина поне осем знака + Силата на паролата не е достатъчна + Достатъчна сила на паролата + Архивирането на устройството не е възможно + Моля, първо настройте резервно копие на устройството. + Отказ + Настройки + Силата на паролата е много добра + Сигурност на приложението + Архивиране на устройството + Моля, изберете поне един метод за архивиране на приложението. + парола + Промяна на паролата + За да изтриете, трябва да бъде установена връзка със сървъра за рецепти + Стоките са готови + Стоките са готови от %s + Стоките са готови току-що + Стоките са готови от %s минути + Стоките са готови от %s + Деактивирано, защото не е въведена парола. + Деактивирано, защото паролата е твърде слаба. + Деактивирано, защото паролите не съвпадат. + Разгледайте + Регистър за донорство на органи + Отваряне на регистъра за даряване на органи? + Ще бъдете пренасочени към регистъра на донорите на органи. За да видите и промените вашите данни за даряване на органи, трябва да влезете там. + Отворете + Отказ + Функцията не е активна в демо режим diff --git a/app/features/src/main/res/values-cs/strings.xml b/app/features/src/main/res/values-cs/strings.xml index e964e395..a3e7e248 100644 --- a/app/features/src/main/res/values-cs/strings.xml +++ b/app/features/src/main/res/values-cs/strings.xml @@ -28,7 +28,7 @@ Nerušit Pojďme Co potřebuješ: - Zadejte přístupové číslo karty + Zadejte přístupové číslo zadejte PIN kód Zkus to znovu Připojení k serveru se nezdařilo. @@ -59,26 +59,23 @@ otisk editor gematik GmbH\n Friedrichstrasse 136\n 10117 Berlín - Jednatel: Dr. Florian Hartge\n Rejstříkový soud: Okresní soud Berlin-Charlottenburg\n Číslo obchodního rejstříku: HRB 96351\n DIČ: DE241843684 + Vedení: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nRejstříkový soud: Okresní soud Berlín-Charlottenburg\nObchodní rejstříkové číslo: HRB 96351\nDIČ: DE241843684 Zodpovědnost za obsah - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Kontakt Oznámení Snažíme se používat genderově spravedlivý jazyk. Pokud si všimnete nějaké chyby, budeme rádi, když se ozvete e-mailem. Německá moderní platforma pro digitální medicínu Napište email Otevřete webovou stránku - Vítejte - Spusťte registraci Odemknout Registrovat zrušení - Bezpečnostní Právní otisk ochrana dat Podmínky použití - Podrobnosti + Podrobnosti receptu Označit jako vyplacené Označit jako neuplatněné Léková forma @@ -100,8 +97,6 @@ Číslo rostliny Telefonní číslo Emailová adresa - Pracovní úraz - Den nehody Číslo havarijní společnosti nebo zaměstnavatele Chcete tento recept trvale smazat? Vymazat @@ -124,7 +119,6 @@ Otevřete skener pro recepty Nastavení Potlačit snímky obrazovky - Zabrání zobrazení náhledu při přepínání aplikací Umožňujete E-Prescription anonymně analyzovat vaše chování při používání? Technické informace Zabezpečení vašich údajů o předpisech @@ -146,23 +140,15 @@ Anonymní analýza zůstává zakázána %s Děkujeme za vaši podporu! Registrovat - Pro stažení receptů se prosím identifikujte. - Poznámka pro lékárny: Kontaktní údaje a informace o lékárnách získáváme z mein-apothekenportal.de německého svazu lékáren Zjistili jste chybu nebo byste chtěli údaje opravit? + Prosím identifikujte se. + Poznámka pro lékárny: Kontaktní údaje a informace o lékárnách získáváme z mein-apothekenportal.de. Zjistili jste chybu nebo chcete údaje opravit? Zjistěte více Lékárny Bohužel to nefungovalo \uD83D\uDE15 Zkuste to znovu. - Zadejte heslo - Dále Přístupnost - Zvětšení - Umožňuje zvětšit aplikaci přiblížením sevřením. - Heslo - Zabezpečte svá data heslem dle vašeho výběru. - Heslo + Povolit zoomování Uložit - Zobrazit heslo - Zopakovat heslo Doporučení: %s Napište email Když odešlete zprávu, přenesou se následující informace o použitém hardwaru a operačním systému: @@ -173,7 +159,7 @@ náklad filtr Filtr - Není k dispozici žádná poloha + Sdílejte polohu Rozuměl Opakovaná hesla se shodují Chyba 20 10 76631 @@ -185,8 +171,6 @@ Bylo zjištěno %s neúspěšných pokusů o přihlášení. - Vyberte nejlepší zálohu zařízení - Může to být otisk prstu, vzor přejetí nebo něco podobného Tokeny Přístupové tokeny SSO tokeny @@ -194,7 +178,7 @@ není k dispozici žádný token jednotného přihlášení zkopírován do schránky Kliknutím zkopírujete token do schránky - Platí pouze dnes + Lze uplatnit pouze dnes Dovolit žádné připojení k serveru Zkuste to znovu za několik minut @@ -262,7 +246,7 @@ %s nových receptů Splatný - Ve vykoupení + Zpracovává se Vykoupeno Neznámý Zobrazit protokoly přístupu @@ -274,7 +258,7 @@ Recept se právě zpracovává a nelze jej smazat Akceptovat Zřejmě to nefungovalo - Jsme si vědomi, že spojení se zdravotní kartou má svá úskalí. V budoucnu by měla být registrace možná i přes již ověřenou aplikaci zdravotního pojištění. \n\n Pracujeme také na zajištění toho, aby bylo možné recepty uplatňovat digitálně bez registrace. \n\n Všimli jste si během tohoto procesu něčeho, o co byste se s námi chtěli podělit? Napište nám, rádi také obdržíme velmi kritickou zpětnou vazbu. + Jsme si vědomi, že spojení se zdravotní kartou má svá úskalí. V budoucnu by měla být registrace možná i přes již ověřenou aplikaci zdravotního pojištění.\n\nPracujeme také na zajištění toho, aby bylo možné recepty uplatňovat digitálně bez registrace.\n\nVšimli jste si během tohoto procesu něčeho, o co byste se s námi chtěli podělit? Napište nám, rádi také obdržíme velmi kritickou zpětnou vazbu. Tipy pro připojení Zlepšete pevnost spojení V případě potřeby sejměte ochranný kryt. @@ -297,7 +281,7 @@ Naskenováno %s Označeno jako uplatněné %s Jak chcete pokračovat? - Objednat + Odeslat do lékárny Brzy dostupný Rezervujte si ji nyní k vyzvednutí nebo si ji nechte doručit kurýrem nebo poštou Uschovejte pro pozdější objednání @@ -329,13 +313,11 @@ https://www.openstreetmap.org/copyright Ochrana dat a použití Dále - PIN ke zdravotní kartě jste obdrželi od své zdravotní pojišťovny bezpečným postupem, jako je Postident. + PIN ke zdravotní kartě jste si museli aktivně objednat u své zdravotní pojišťovny a poté jej obdržet bezpečným procesem, jako je Postident. Nebyl přijat žádný PIN PIN kód Zkontrolujte připojení zařízení k internetu a nastavení času a data. - Pro přihlášení stiskněte „Odemknout“. Uzamčen? Ověřte prosím své biometrické údaje na tomto zařízení. - Zapomenuté heslo? Smažte aplikaci a poté ji znovu nainstalujte. Proč tomu tak je, můžete zjistit v našem %s . oblast pomoci Velikost balení a jednotka aktivní složka @@ -348,10 +330,6 @@ Nehotový Oznámení Pomozte nám tuto aplikaci vylepšit - Zadejte heslo - Heslo musí mít alespoň osm znaků - Síla hesla není dostatečná - Síla hesla je dostatečná Heslo je viditelné Heslo není viditelné biometrie @@ -458,9 +436,6 @@ Právě odesláno Odesláno v %s čas Nadále není platné - Zaregistrujte se pomocí aplikace - Vyberte pojištění - Nenašli jste, co jste hledali? Tento seznam se neustále rozšiřuje. Registraci se zdravotním průkazem již podporuje každá zdravotní pojišťovna. Zpětná vazba z aplikace e-recept Těšíme se na vaši zpětnou vazbu. Použijte prosím následující mezeru a buďte co nejpřesnější: PUK @@ -480,7 +455,7 @@ Oznámení Akceptovat Zabezpečení vašich údajů o předpisech - \\"Tato aplikace používá nejbezpečnější biometrický senzor poskytovaný vaším zařízením k zabezpečení vašich přihlašovacích údajů v chráněné oblasti úložiště zařízení.\" + \Tato aplikace používá nejbezpečnější biometrický senzor poskytovaný vaším zařízením k zabezpečení vašich přihlašovacích údajů v chráněné oblasti úložiště zařízení.\ Biometrické zabezpečení vašich přístupových údajů vám umožňuje otevřít tuto aplikaci v budoucnu, prohlížet, načítat, uplatňovat nebo mazat recepty bez zdravotní karty a zadání vašeho PIN. Zajistěte prosím, aby lidé, se kterými můžete toto zařízení sdílet a jejichž biometrické charakteristiky mohou být v tomto zařízení uloženy, měli také přístup k vašim předpisům. což se bohužel nepovedlo @@ -493,7 +468,7 @@ Příjmení Pojištění cislo pojistence - Přístupové číslo karty + Přístupové číslo Registrovat Odhlásit se Uložit @@ -509,8 +484,7 @@ spojení ztraceno Chcete se nyní připojit k serveru receptů? Žádné žetony - Token obdržíte, když se přihlásíte do služby předpisu.\n - Objednávky + Token obdržíte, když se přihlásíte do služby předpisu. Vyberte požadovaný PIN Odemknout kartu Vyberte PIN @@ -527,11 +501,11 @@ Kód vyzvednutí přijat Zprávu nelze zobrazit Kontaktujte prosím svou lékárnu ( %s ). - Zobrazit odkaz na nákupní košík + Odkaz na lékárnu Zobrazit kód vyzvednutí Ukažte zprávu %s v %s hodin - Recept odeslán na %s . Proces digitálního výkupu je pro mnoho lékáren stále neznámý. Pokud se do zítřka neozvete, doporučujeme preventivně zavolat a zeptat se. + Recept odeslán na %s . Některé lékárny zatím nemají možnost digitální odpovědi. Pokud do zítřka nedostanete žádnou odpověď, preventivně zavolejte. Přehled objednávek Nový Chod @@ -542,11 +516,11 @@ Požadovaný PIN byl uložen Momentálně otevřené a blízko mě Filtrovat podle … - spustit vyhledávání + Hledat Přímé zadání Lékárny telefonní číslo (volitelné) - Hledejte podle jména nebo adresy + Hledat Žádné platné informace o lékárně O této lékárně nebyly nalezeny žádné aktuální informace. Záznam pro tuto lékárnu bude smazán. OK @@ -566,16 +540,15 @@ Vylepšení produktu Anonymní analýza Pomozte nám tuto aplikaci vylepšit. Veškeré údaje o používání jsou shromažďovány anonymně a slouží výhradně ke zlepšení uživatelské zkušenosti. - Zabezpečení aplikace osobní nastavení Přístupnost Vylepšení produktu Přidán recept Recept je již k dispozici Při importu došlo k chybě + Přístupové číslo Vymazat Naskenovaný recept - Možnost přípravy na výměnu Zapomenutý PIN %s Recept @@ -593,7 +566,7 @@ Toto rozhodnutí můžete kdykoli změnit v nastavení systému. Pokračovat Akceptovat - Tato aplikace používá nejbezpečnější metodu poskytovanou vaším zařízením. + Aplikace používá nejbezpečnější metodu, kterou jste na svém zařízení nastavili. Uložit Vybrat lék @@ -621,15 +594,13 @@ Co je to přímé zadání? S přímým doporučením je recept z praxe nebo nemocnice vyplněn přímo v lékárně. Pojištěnci nemusejí činit žádnou akci a nemohou zasahovat do procesu odkupu. \n\n Přímá doporučení jsou uvedena v aplikaci e-recept, aby pro vás byla vaše léčba transparentnější. Poplatek za pohotovostní službu - Někdy je nutný spěch. Některé recepty lze vyplnit bez doplatku poplatku za pohotovostní službu, například v noci nebo o svátcích. + Pokud je recept vyplněn mezi 20:00 a 6:00 nebo v neděli ao svátcích, může být účtován příplatek 2,50 EUR. Léky podléhající spoluúčasti Osvobozeno od doplatku - Osoby se zákonným zdravotním pojištěním musí za léky na předpis doplatit až deset eur. \n\n Výše doplatku závisí na ceně vašeho léku. Léky, které stojí méně než 5 EUR, si musíte zaplatit sami.\n Za léky, které jsou dražší, musíte zaplatit deset procent z ceny, minimálně však 5 € a maximálně 10 €. \n\n Děti a mládež do 18 let jsou obecně osvobozeni od doplatku. \n\n Pokud vaše roční náklady na léky překročí limit finanční zátěže, můžete být od spoluúčasti osvobozeni. Promluvte si o tom se svou zdravotní pojišťovnou. + Lidé se zákonným zdravotním pojištěním obvykle platí za léky na předpis maximálně 10 eur. Vyšší poplatky mohou být účtovány, pokud je požadován lék od konkrétního výrobce, na který se nevztahuje smlouva o slevě zdravotního pojištění („žádost léku“). \n\n Děti a mladiství do 18 let jsou od doplatků osvobozeni. \n\n Pokud jsou recepty proplaceny později než 28 dnů po jejich vystavení, musí být náklady uhrazeny v plné výši. \n\n Pokud přes rok utratíte hodně léků, můžete si u své zdravotní pojišťovny zažádat o osvobození od doplatku Jste osvobozeni od placení spoluúčasti za tento lék. Náklady na léky uhradí vaše zdravotní pojišťovna. Jak dlouho je tento předpis platný? Během tohoto období můžete svůj recept uplatnit v jakékoli lékárně s maximálním doplatkem 10 EUR. - Možnost přípravy na výměnu - Na základě zákonných požadavků vaší zdravotní pojišťovny vám může být poskytnuta alternativa se stejnou účinnou látkou. \n\n Léky mohou vypadat a jmenovat se různě, mít různé ceny a výrobce, ale stále obsahují stejnou účinnou látku. Pro účinek léků v organismu je rozhodující samotná účinná látka a dávkování. Pacienti často dostanou v lékárně jiný lék, než jaký předepsal lékař – pokud jsou léky srovnatelné. Pro změnu mohou existovat terapeutické a ekonomické důvody. Naskenovaný recept Předpisy importované z tištěné kopie nemohou z bezpečnostních důvodů zobrazovat osobní nebo lékařské informace. \n\n Přihlaste se do této aplikace pomocí zdravotní karty nebo pojišťovací aplikace a zobrazte všechny informace obsažené v předpisu. Recept nesprávný @@ -639,7 +610,7 @@ telefon webová stránka Pošta - Třídění podle vzdálenosti není možné. + Sdílejte svou polohu, abyste našli lékárny ve svém okolí. OK Zadejte aktuální PIN Zadán nesprávný PIN @@ -657,12 +628,10 @@ Zdravotní průkaz biometrie Nepřihlášen - Váš názor nás zajímá. Věnujte prosím pět minut vyplnění našeho průzkumu. Předem moc děkuji. Upozornění Lékárna přidána do oblíbených Lékárna odstraněna z oblíbených Moje lékárny - Síla hesla velmi dobrá Operace zápisu nebyla úspěšná PIN se nepodařilo uložit Zpráva @@ -681,7 +650,6 @@ Uplatněno %s Vykoupeno právě teď Uplatněno v %s hodin - Objednávky Tento předpis byl vyplněn jako součást léčby pro vás. Poplatek za pohotovostní službu Tento recept nelze vyplnit v lékárně v noci bez doplacení poplatku za pohotovostní službu. @@ -695,7 +663,7 @@ Dostávat recepty digitálně? Pro obnovení stáhněte obrazovku dolů. Žádné recepty - Přihlaste se, abyste dostávali recepty automaticky, nebo přidejte nový recept pomocí ⊕ v horním rohu. + Přihlaste se k automatickému odběru receptů. Registrovat Archiv receptů Možná později @@ -732,9 +700,9 @@ Klepnutím na displej přeskočíte zobrazený popis. Jak vykoupit? Jak byste chtěli dostávat své léky? - Uplatnit přímo - Uplatněte léky na místě - Objednat + Zobrazit kód + Nechte si to naskenovat v lékárně + Odeslat do lékárny Rezervujte nebo nechte dovézt Připraveno Kód sbírky @@ -802,7 +770,7 @@ Tím se vymažou všechny účtenky z tohoto zařízení a serveru. Přijímat potvrzení o nákladech Vaše účtenky jsou také uloženy na receptovém serveru. - Přijato + aktivovat Celkem: %s %s Vybrat Rozdělit @@ -851,17 +819,17 @@ Je vyžadován přidružený PIN Lze uplatnit pouze zítra jako samoplátce Zbývá pouze %s dní na uplatnění jako samoplátce - \nStále lze uplatnit jako samoplátce po dobu %s dnů\n - Platí pouze %s dnů - \nPlatné zbývá %s dnů\n - Platí pouze zítra + Stále lze uplatnit jako samoplátce po dobu %s dnů + Na uplatnění zbývá pouze %s dnů + Stále lze uplatnit po dobu %s dnů + Lze uplatnit pouze zítra Účtují se poplatky Bere pojištění Recepty byly úspěšně přeneseny. Recept nelze zpracovat. Prosím zkuste to znovu. Možná budete muset vybrat jinou lékárnu. Recept nelze zpracovat. Lékárna hlásí neznámou chybu. V případě potřeby zkuste jinou lékárnu. Předpis byl lékárnou zamítnut. Předpis může být neplatný nebo vaše doručovací adresa nebo kontaktní údaje mohou být neplatné. - Nelze uplatnit, zkontrolujte prosím připojení k internetu. + Zkuste to znovu a případně zvolte jinou lékárnu. Pokud chyba přetrvává, kontaktujte podporu. Recept byl úspěšně přenesen. Lékárna však hlásí chybu zpracování. Kontaktujte prosím lékárnu. Předpis byl lékárnou zamítnut. Předpis byl již uplatněn. Předpis byl lékárnou zamítnut. Recept byl smazán. @@ -894,7 +862,6 @@ Aktivujte demo režim Teď ne Vyhledávání - Je vyžadován smartphone s podporou NFC Přijímejte potvrzení o nákladech digitálně Jakmile budou účtenky aktivovány, najdete je zde po uplatnění svého předpisu. aktivovat @@ -902,11 +869,11 @@ Jakmile lékárna uloží doklad o nákladech, objeví se zde. Potvrzení o nákladech Přijímejte doklady o nákladech digitálně - Poznámka: Stvrzenky o výdajích již nebudete dostávat jako výtisk v\n LÉKÁRNA.\n + Poznámka: Účtenky o nákladech již nebudete dostávat jako výtisk v lékárně. aktivovat Možná později dohoda - S vaším souhlasem se zdržíte tisku v lékárně\n a v budoucnu odesílat své doklady o nákladech digitálně.\n + S vaším souhlasem se zdržíte jejich tisku v lékárně a v budoucnu budete své účtenky zasílat digitálně. zrušení Souhlas Ukončete demo režim @@ -921,17 +888,17 @@ Žádný internet Chybí připojení k internetu. Nesprávný požadavek - Vyskytl se problém s požadavkem. Pracujeme na řešení.\n + Vyskytl se problém s požadavkem. Pracujeme na řešení. server neodpovídá Zkuste to znovu za několik minut. Nepodařilo se připojit - V současné době nelze získat žádné informace. Prosím\n Zkuste to později znovu.\n + V současné době nelze získat žádné informace. Prosím zkuste to znovu později. Odmítnuto - Server vaši žádost odmítl. Zkuste to prosím\n znovu později.\n + Server vaši žádost odmítl. Prosím zkuste to znovu později. Aktualizovat aplikaci - Chcete-li tuto funkci používat, aktualizujte si aplikaci.\n + Chcete-li tuto funkci používat, aktualizujte si aplikaci. Přihlášení selhalo - Server měl problém s přihlášením. Prosím, kontaktujte nás\n znovu.\n + Server měl problém s přihlášením. Přihlaste se prosím znovu. Registrovat Kopírovat URL Připojte se k externí aplikaci zdravotního pojištění. @@ -957,12 +924,287 @@ Doporučeno Digitální zdravotní ID Upravit nastavení? - Stránka 404? Chcete-li pokračovat, povolte otevírání odkazů v nastavení. Otevřete Nastavení Zastaralá verze aplikace Vaše verze aplikace je zastaralá a již není podporována. Chcete-li nadále používat e-recept, aktualizujte aplikaci. Aktualizovat - Vaše CAN má %s číslic. + Vaše přístupové číslo má %s číslic. Váš PIN může mít %s až %s číslic. + Vyzvednout + Kurýr + náklad + Je vyžadován smartphone s podporou NFC + Požadavek na smazání receptu se nám nepodařilo zpracovat. Pracujeme na řešení ( %s ). + Při mazání mého předpisu se něco pokazilo, obdržel jsem kód chyby %s . Upozorněte mě prosím, až bude možné smazání znovu. + Dále + Nesprávný požadavek + Zpráva + Smazat lokálně + Žádný internet + Recept se nepodařilo smazat. Zkontrolujte prosím připojení k internetu a zkuste to znovu. + Zkus to znovu + OK + Znovu se přihlásit + Pro smazání receptu musíte být přihlášeni (401). + Registrovat + zrušení + Smazání nemožné + V tuto chvíli nemůžete předpis smazat, protože je v lékárně k vyplnění nebo se jedná o nevyřízené přímé přiřazení (403). + Příliš mnoho požadavků + Příliš mnohokrát jste se pokusili smazat recept. Zkuste to znovu později (429). + Smazat recept + Jejda... + "Došlo k neočekávané chybě serveru. Zkuste to znovu později (500)." + Zabezpečení aplikace není možné. Předtím si na svém zařízení nastavte biometrické zabezpečení (např. otisk prstu). + Biometrické zálohování není pro toto zařízení možné, protože jej vaše zařízení nepodporuje. + Nebezpečí! Váš telefon není dobře chráněn. + Vaše přihlašovací údaje jsou uloženy ve vašem telefonu. Pro ochranu přístupu se používá preferovaný způsob přihlášení. Tento způsob přihlášení, např. vzor přejetí prstem, není příliš bezpečný. Funkci „Uložení přihlašovacích údajů“ používáte na vlastní riziko. + Pokud stále používáte funkci „Uložit přihlašovací údaje“, budete moci v budoucnu prohlížet, přistupovat, uplatňovat nebo mazat recepty pomocí aplikace e-recept bez zdravotní karty a zadání PIN. + Poloha nenalezena + Odeslat do lékárny + Zobrazit kód + Jazyk + Jazyk + Němec + Arabština + Bulharský + Çeština + Dánština + Angličtina + Francouzština + Hebrejština + Italština + Holandský + Polština + Rumunština + Ruština + Turečtina + Ukrajinština + výchozí + Digitální účtenky byly deaktivovány a smazány. + „Fórum s elektronickými recepty“ + Není možná náhradní příprava + Možnost přípravy na výměnu + Náhradní příprava (Aut idem) + Lékárníci jsou povinni přednostně vydávat léky, na které má zdravotní pojišťovna pacienta uzavřenu slevovou smlouvu s výrobci léků. To neplatí pouze v případě, že lékař na předpisu vyloučí „Aut idem“, což není případ vašeho předpisu. + Váš lékař rozhodl, že byste měli dostat předepsaný přípravek. Lékárna by neměla provádět výměnu na základě dohody o slevě („Aut idem“). + Povolit snímky obrazovky + Pokud povolíte snímky obrazovky, poslední stránka, kterou jste otevřeli, zůstane při přepínání aplikací viditelná na pozadí. V případě potřeby mohou být vaše osobní údaje viditelné. Doporučujeme proto nepovolovat snímky obrazovky. + Dovolit + zrušení + Přepněte klávesnici na nálepky nebo použijte emotikony jako profilový obrázek. + Vyberte profilový obrázek + Jak chcete pokračovat? + Vyberte fotografii + Fotoaparát + Emoji + zrušení + Otevřete Nastavení + Použití + vyfotit + Upravit profilový obrázek + Převzato před %s minutami + Přijato dne %s + Právě přijato + Přijato v %s hodin + HealthID + Vyberte pojištění + Pokud registrace pomocí Health ID neproběhne podle očekávání, postupujte podle tipů z našeho centra nápovědy. + Pomoc + Pomoc + Tipy pro registraci v aplikaci pojištění + Za zdravotní průkaz odpovídá vaše pojišťovna. Pokud máte nějaké dotazy ohledně registrace, kontaktujte je. Zde je několik osvědčených tipů: + Upozorňujeme, že v závislosti na vašem pojištění může být vyžadována samostatná aplikace. Obraťte se prosím u své pojišťovny, co to je. + Spusťte aplikaci pokladny a před zahájením registrace v aplikaci e-recept se tam jednou přihlaste. + Přepínání mezi registrací pomocí zdravotního průkazu a zdravotní karty může vést k problémům. Před změnou možnosti přihlášení se proto nejprve aktivně odhlaste ze svého profilu. + Pokud vaše pojištění není na seznamu, můžete se alternativně zaregistrovat pomocí zdravotní karty a příslušného PIN. + Pokud vás pokladní aplikace nepřesměruje zpět do aplikace e-receptu, nezapomeňte tuto chybu nahlásit své pojišťovně. + Pokud aplikaci pojištění nelze získat, může být užitečné přejít do nastavení prohlížeče smartphonu a povolit otevírání odkazů. + Pokud váš smartphone používá Android 14, může být užitečné povolit otevírání odkazů v nastavení. + Otevřete Nastavení + což se bohužel nepovedlo + Prosím zkuste to znovu později. + Chybové zprávy při načítání. + Vyberte emodži nebo text + Uložit + Pro pokračování se prosím přihlašte + Dále + Volání + Napsat\nPošta + Do lékárny + LÉKÁRNA + Dnes + Zítra + Trasa\nzde + Smazat předpis a potvrzení o nákladech + Pokud vymažete recept, vymaže se také související účtenka. + Zprávy ze všech profilů se nyní zobrazují společně + Nové vlastnosti + Nové vlastnosti + Neustále pracujeme na nových funkcích. Podělte se s námi o svůj názor a pomozte nám vytvořit lepší aplikaci. + Objednávky pro každého + V části Objednávky nyní můžete vidět objednávky pro všechny profily společně a mnohem snadněji je prohlížet + + Žádná náhrada nákladů + Zdravotní pojištění zpravidla nehradí náklady na tento recept. Jako pacient jste tedy odpovědní za plnou úhradu. Zda jsou náklady hrazeny v rámci připojištění nebo zákonných plnění, je nutné individuálně prověřit. + Vaše pojištění nebude hradit žádné náklady na tento předpis. + + "Vaše pojištění nepokryje předpis %s " + + + "Vaše pojištění nepokryje předpisy %s ." + + Způsobeno + Datum + Nehoda + Pracovní úraz + Průmyslová nemoc + Zprávy + Žádné novinky + Zatím nemáte žádné zprávy. + 🎉 Vaše objednávka je připravena k vyzvednutí. Ukažte prosím tento kód pro vyzvednutí, abyste se identifikovali. + Zpráva z vaší lékárny byla bohužel prázdná. Kontaktujte prosím svou lékárnu. + Lékárna vám poskytla odkaz. + Zprávy + Objednávka + Zprávy + Žádné připojení k internetu + Chcete-li zobrazit připojená zařízení, musíte být připojeni k biometrii. + Předpis byl smazán dne %s + Smazáno + Žádné recepty + V archivu nemáte žádné recepty. + Vaše účtenky o výdajích byly smazány + Potvrzení o nákladech + Je nutná registrace + Pro zobrazení připojených zařízení se prosím přihlaste. + Registrovat + Registrovat + Připojte se k pojištění %s + Zdravotní karta na smartphonu + Nejste přihlášeni + Registrovaný + osvěžit + Přístupové protokoly obdržíte, pokud jste přihlášeni ke službě předpisu. + Je nutná registrace + Přihlaste se prosím pomocí své zdravotní karty a uložte své přihlašovací údaje s biometrií, abyste mohli zobrazit připojená zařízení. + %1$s x Dobré ráno + %1$s x oběd + %1$s x večer + %1$s x V noci + Pokud vám lékař nedal jiné pokyny, pokyny k použití lze chápat takto: + Tuto informaci o užívání léku vám poskytl váš lékař. + Na vašem receptu nejsou žádné informace o tom, jak lék užívat. + Váš lékař si všiml, že jste dostali pokyny, jak užívat tento lék, který není na lékařský předpis. Může to být například ve vašem léčebném plánu. + Nespecifikováno + DJ + Návod k použití + " %s - %s" + Připomenutí příjmů + Vezměte si připomenutí + Orální léky + O připomenutích příjmu + Ráno + Polední + Odpoledne + Večer + lék + Informace o dávkování + 1 + %s / %s + 1 dávka + Změňte dávkování + zrušení + Připomenutí léků + Harmonogram léků + Užívejte dávkování podle lékařského předpisu + V %s hodin trvat %s x + Vyberte data + Další + Zrušit + A + Mimo + Sledování + Neomezený + Jednotlivě + První den + Poslední den + Přidejte čas + Žádné připomenutí příjmu + Pro své recepty si můžete nastavit připomenutí + Zapnuto + vypnuto + Pamatuj si mě + Vypněte optimalizaci baterie pro tuto aplikaci. + Pokud tuto možnost neaktivujete, mohou se připomenutí léků jevit jako nespolehlivé. + zrušení + Povolit + Návod k použití + Nespecifikováno + informace + Pamatujte také na režim optimalizace baterie + Změňte dávkování + Dav + formulář + skončilo + neomezený + do %s + Opakovat %s + Kde je to možné, používáme pro výpočet informace uložené v předpisu. + zrušení + Uložit + Až do konce balení + Čas + dávka + Zobrazit léky + Už jste si vzal(a) léky? + Vezměte si připomenutí + Žádné aktivní vzpomínky + Dnes nemáte žádná připomenutí + Vyberte datum zahájení + Vyberte datum ukončení + Vítejte + ve vaší aplikaci elektronického receptu + Zkuste to znovu + Zadejte heslo + Ujistěte se prosím, že kdokoli, s kým můžete toto zařízení sdílet a kdo zná vaše přihlašovací údaje, má také přístup k vašim receptům. + Zadejte heslo + Chcete-li aplikaci odemknout, zadejte heslo. + heslo + Heslo + Pro zabezpečení aplikace prosím zadejte heslo. + Zobrazit heslo + Opakujte heslo + Zapomněli jste heslo? Smažte aplikaci a poté ji znovu nainstalujte. Proč tomu tak je, můžete zjistit v našem %s. + Zadejte heslo + Heslo musí mít alespoň osm znaků + Síla hesla není dostatečná + Síla hesla je dostatečná + Zálohování zařízení není možné + Nejprve prosím nastavte zálohu zařízení. + Zrušit + Nastavení + Síla hesla velmi dobrá + Zabezpečení aplikace + Záloha zařízení + Vyberte prosím alespoň jeden způsob zálohování aplikace. + heslo + Změnit heslo + Chcete-li odstranit, musí být vytvořeno připojení k serveru Prescription Server + Zboží je připraveno + Zboží je připraveno od %s + Zboží je právě připraveno + Zboží je připraveno %s minut + Zboží je připraveno od %s + Zakázáno, protože není zadáno žádné heslo. + Zakázáno, protože heslo je příliš slabé. + Deaktivováno, protože hesla se neshodují. + Prozkoumat + Registr darování orgánů + Otevřít registr dárcovství orgánů? + Budete přesměrováni do registru dárců orgánů. Chcete-li zobrazit a změnit údaje o dárcovství orgánů, musíte se tam přihlásit. + OTEVŘENO + Zrušit + Funkce není aktivní v demo režimu diff --git a/app/features/src/main/res/values-da/strings.xml b/app/features/src/main/res/values-da/strings.xml index 1a434a35..ec91540e 100644 --- a/app/features/src/main/res/values-da/strings.xml +++ b/app/features/src/main/res/values-da/strings.xml @@ -26,7 +26,7 @@ Fortryd ikke Lad os gå Hvad du har brug for: - Indtast kortadgangsnummer + Indtast adgangsnummer indtaste PIN-koden Prøv igen Kunne ikke oprette forbindelse til serveren. @@ -55,26 +55,23 @@ aftryk redaktør gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin - administrerende direktør: Dr. Florian Hartge\n Registreringsret: Berlin-Charlottenburg District Court\n Handelsregisternummer: HRB 96351\n Momsidentifikationsnummer: DE241843684 + Ledelse: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nRegistreringsret: District Court of Berlin-Charlottenburg\nHandelsregisternummer: HRB 96351\nMomsidentifikationsnummer: DE241843684 Ansvarlig for indholdet - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Kontakt Varsel Vi bestræber os på at bruge et ligestillet sprog. Hvis du bemærker fejl, hører vi gerne fra dig via e-mail. Tysklands moderne platform for digital medicin Skriv e-mail Åben hjemmeside - Velkommen - Start registreringen Lås op Tilmeld Afbestille - Sikkerhed gyldige aftryk data beskyttelse Vilkår for brug - detaljer + Opskrift detaljer Markér som indløst Markér som uindløst Doseringsform @@ -96,8 +93,6 @@ Anlægsnummer Telefon nummer Email adresse - Arbejdsulykke - Ulykkens dag Ulykkesvirksomheds- eller arbejdsgivernummer Vil du slette denne opskrift permanent? Slet @@ -120,7 +115,6 @@ Åbn scanner for recepter Indstillinger Undertryk skærmbilleder - Forhindrer et eksempelbillede i at blive vist, når der skiftes app Tillader du E-Recept at analysere din brugsadfærd anonymt? Teknisk information Sikkerhed for dine receptdata @@ -142,23 +136,15 @@ Anonym analyse forbliver deaktiveret %s Tak for din støtte! Tilmeld - Identificer dig selv for at downloade opskrifter. - Bemærkning til apoteker: Vi indhenter kontaktoplysninger og information om apoteker fra mein-apothekenportal.de fra den tyske apotekerforening Har du opdaget en fejl eller vil du rette data? + Identificer dig venligst. + Bemærkning til apoteker: Vi indhenter kontaktoplysninger og information om apoteker fra mein-apothekenportal.de. Har du opdaget en fejl eller vil du rette data? Lær mere Apoteker Desværre virkede det ikke \uD83D\uDE15 Prøv det igen. - Indtast adgangskode - Yderligere Tilgængelighed - Zoom - Giver dig mulighed for at forstørre appen ved at knibe for at zoome. - adgangskode - Sikre dine data med en adgangskode efter eget valg. - adgangskode + Aktiver zoom Gemme - Vis adgangskode - Gentag adgangskode Anbefalinger: %s Skriv e-mail Når du sender din besked, overføres følgende oplysninger om den anvendte hardware og operativsystem: @@ -169,7 +155,7 @@ Forsendelse filter Filter - Ingen tilgængelig placering + Del placering Forstået Gentagne password-matches Fejl 20 10 76631 @@ -179,8 +165,6 @@ %s mislykkede loginforsøg blev fundet. %s mislykkede loginforsøg blev fundet. - Vælg den bedste sikkerhedskopiering af enheden - Dette kan være et fingeraftryk, swipe-mønster eller noget lignende Poletter Adgangstokens SSO-tokens @@ -188,7 +172,7 @@ intet tilgængeligt SSO-token kopieret til udklipsholderen Klik for at kopiere tokenet til udklipsholderen - Gælder kun i dag + Kan kun indløses i dag Give lov til ingen forbindelse til serveren Prøv venligst igen om et par minutter @@ -254,7 +238,7 @@ %s nye opskrifter Indløses - I forløsning + Er under behandling Forløst Ukendt Se adgangslogfiler @@ -266,7 +250,7 @@ Opskriften er i gang og kan ikke slettes Acceptere Det virkede åbenbart ikke - Vi er klar over, at sammenhængen med sundhedskortet har sine faldgruber. Fremover skal registrering også være mulig via en allerede godkendt sygesikringsapp. \n\n Vi arbejder også på at sikre, at recepter kan indløses digitalt uden tilmelding. \n\n Har du bemærket noget under denne proces, som du gerne vil dele med os? Skriv venligst til os, vi vil også gerne modtage meget kritisk feedback. + Vi er klar over, at sammenhængen med sundhedskortet har sine faldgruber. Fremover skal registrering også være mulig via en allerede godkendt sygesikringsapp.\n\nVi arbejder også på at sikre, at recepter kan indløses digitalt uden tilmelding.\n\nLagde du mærke til noget under denne proces, som du gerne vil dele med os? Skriv venligst til os, vi vil også gerne modtage meget kritisk feedback. Tilslutningstips Forbedre styrken af ​​forbindelsen Fjern om nødvendigt beskyttelsesdækslet. @@ -289,7 +273,7 @@ Scannet den %s Markeret som indløst den %s Hvordan vil du fortsætte? - Bestille + Send til apoteket Tilgængelig snart Reserver nu til afhentning eller få det leveret med kurer eller forsendelse Gem til senere bestilling @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Databeskyttelse og brug Yderligere - Du har modtaget PIN-koden til dit sundhedskort fra dit sygesikringsselskab ved hjælp af en sikker procedure som Postident. + Du skulle aktivt bestille din sundhedskort-pinkode hos dit sygesikringsselskab og derefter modtage den via en sikker proces som Postident. Ingen pinkode modtaget pinkode Tjek din enheds internetforbindelse og indstillinger for tid/dato. - For at logge på, tryk på \"Lås op\". Låst ude? Bekræft venligst dine biometriske legitimationsoplysninger på denne enhed. - Glemt kodeord? Slet venligst appen og geninstaller den. Du kan finde ud af hvorfor i vores %s . hjælpeområde Pakkestørrelse og enhed aktiv ingrediens @@ -338,10 +320,6 @@ Fortryd Varsel Hjælp os med at gøre denne app bedre - Indtast adgangskode - Adgangskoden skal være mindst otte tegn lang - Adgangskodestyrken er ikke tilstrækkelig - Adgangskodestyrke tilstrækkelig Adgangskoden er synlig Adgangskoden er ikke synlig biometri @@ -446,9 +424,6 @@ Sendt lige nu Sendt kl. %s Ikke længere gyldig - Tilmeld dig med app - Vælg forsikring - Fandt du ikke det, du ledte efter? Denne liste bliver konstant udvidet. Registrering med et sundhedskort understøttes allerede af alle sygeforsikringsselskaber. Feedback fra e-recept-appen Vi ser frem til din feedback. Brug venligst følgende plads og vær så præcis som muligt: PUK @@ -468,7 +443,7 @@ Varsel Acceptere Sikkerhed for dine receptdata - \"Denne app bruger den mest sikre biometriske sensor fra din enhed til at sikre dine legitimationsoplysninger i et beskyttet område af enhedens lager.\" + \Denne app bruger den mest sikre biometriske sensor fra din enhed til at sikre dine legitimationsoplysninger i et beskyttet område af enhedens lager.\ Den biometriske sikkerhed af dine adgangsdata giver dig mulighed for at åbne denne app i fremtiden, se, hente, indløse eller slette recepter uden et sundhedskort og indtaste din pinkode. Sørg for, at personer, som du må dele denne enhed med, og hvis biometriske karakteristika kan være gemt på denne enhed, også har adgang til dine recepter. det virkede desværre ikke @@ -481,7 +456,7 @@ Efternavn Forsikring Forsikringsnummer - Kortadgangsnummer + Adgangsnummer Tilmeld Log ud Gemme @@ -497,8 +472,7 @@ forbindelse afbrudt Vil du oprette forbindelse til opskriftsserveren nu? Ingen tokens - Du modtager et token, når du er logget ind på receptservicen.\n - Ordre:% s + Du modtager et token, når du er logget ind på receptservicen. Vælg den ønskede PIN-kode Lås kortet op Vælg PIN @@ -515,26 +489,26 @@ Afhentningskode modtaget Meddelelsen kan ikke vises Kontakt venligst dit apotek ( %s ). - Vis indkøbskurvlink + Apotek link Vis afhentningskode Vis beskeden %s klokken %s - Opskrift sendt til %s . Den digitale indløsningsproces er stadig ukendt for mange apoteker. Hvis du ikke hører tilbage i morgen, anbefaler vi, at du ringer for at forhøre dig som en sikkerhedsforanstaltning. + Opskrift sendt til %s . Nogle apoteker har endnu ikke en digital svarmulighed. Hvis der ikke er noget svar i morgen, bedes du ringe for en sikkerheds skyld. Ordreoversigt Ny Rute Rækkefølgen - Gratis for den, der ringer op. Tjenestetider: Man - Fre 8:00 - 20:00 undtagen på nationale helligdage + Gratis for den, der ringer op. Tjenestetider: Man - Fre 8:00 - 20:00 undtagen på føderale helligdage Apotek Vælg den ønskede PIN-kode Ønsket pinkode gemt I øjeblikket åben og i nærheden af ​​mig Sorter efter … - start søgning + Søge Direkte opgave Apoteker Telefonnummer (valgfrit) - Søg på navn eller adresse + Søge Ingen gyldige apoteksoplysninger Der blev ikke fundet nogen aktuelle oplysninger om dette apotek. Indgangen til dette apotek vil blive slettet. Okay @@ -554,16 +528,15 @@ Produktforbedringer Anonym analyse Hjælp os med at gøre denne app bedre. Alle brugsdata indsamles anonymt og bruges udelukkende til at forbedre brugeroplevelsen. - App sikkerhed personlige indstillinger Tilgængelighed Produktforbedringer Tilføjet opskrift Opskriften er allerede tilgængelig Der opstod en fejl under import + Adgangsnummer Slet Scannet opskrift - Udskiftningsforberedelse mulig Glemt pinkode %s Opskrift @@ -579,7 +552,7 @@ Du kan til enhver tid ændre denne beslutning i systemindstillingerne. Blive ved Acceptere - Denne app bruger den sikreste metode fra din enhed. + Appen bruger den sikreste metode, du har konfigureret på din enhed. Gemme Vælge medicin @@ -607,15 +580,13 @@ Hvad er en direkte opgave? Med direkte henvisning udfyldes en recept fra en praksis eller hospital direkte på et apotek. Forsikrede behøver ikke at foretage sig noget og kan ikke gribe ind i indløsningsprocessen. \n\n Direkte henvisninger er angivet i e-recept-appen for at gøre din behandling mere gennemsigtig for dig. Akut servicegebyr - Nogle gange er hastværk nødvendigt. Nogle recepter kan udfyldes uden yderligere betaling af et akutgebyr, for eksempel om natten eller på helligdage. + Hvis en recept udfyldes mellem kl. 20.00 og 06.00 eller på søn- og helligdage, kan der opkræves et ekstra gebyr på 2,50 euro. Medicin mod egenbetaling Fritaget for yderligere betaling - Dem med lovpligtig sygesikring skal betale en ekstra betaling på op til ti euro for receptpligtig medicin. \n\n Størrelsen af ​​den ekstra betaling afhænger af prisen på din medicin. Du skal selv betale for medicin, der koster mindre end 5 €.\n For medicin, der er dyrere, skal du betale ti procent af prisen, dog mindst 5 € og maksimalt 10 €. \n\n Børn og unge under 18 år er generelt fritaget for merbetaling. \n\n Hvis dine årlige udgifter til medicin overstiger din økonomiske byrdegrænse, kan du blive fritaget for egenbetalingen. Tal med dit sygeforsikringsselskab om dette. + Personer med lovpligtig sygesikring betaler normalt maksimalt 10 euro for receptpligtig medicin. Der kan opkræves højere gebyrer, hvis der anmodes om et lægemiddel fra en bestemt producent, som ikke er dækket af en sygesikringsrabataftale (\"anmodning om lægemiddel\"). \n\n Børn og unge under 18 år er fritaget for tillægsbetaling. \n\n Indløses recepter senere end 28 dage efter, at de er udstedt, skal omkostningerne afholdes fuldt ud. \n\n Bruger du meget medicin i løbet af året, kan du søge om fritagelse for egenbetalingen hos dit sygesikringsselskab Du er fritaget for at betale egenbetaling for denne medicin. Dit sygeforsikringsselskab vil dække udgifterne til medicinen. Hvor længe er denne recept gyldig? I denne periode kan du indløse din recept på ethvert apotek med en maksimal ekstra betaling på €10. - Udskiftningsforberedelse mulig - På grund af lovkrav fra dit sygeforsikringsselskab kan du få et alternativ med det samme aktive stof. \n\n Medicin kan se ud og hedde anderledes, have forskellige priser og producenter, men stadig indeholde det samme aktive stof. Selve det aktive stof og doseringen er afgørende for lægemidlers virkning i kroppen. Patienterne får ofte en anden medicin på apoteket end den, lægen har ordineret – forudsat at medicinen er sammenlignelig. Der kan være terapeutiske og økonomiske årsager til ændringen. Scannet opskrift Recepter importeret fra en papirkopi kan ikke vise personlige eller medicinske oplysninger af sikkerhedsmæssige årsager. \n\n Log ind på denne app med sundhedskort eller forsikringsapp for at se alle oplysningerne i recepten. Opskriften er forkert @@ -625,7 +596,7 @@ telefon internet side Post - Det er ikke muligt at sortere efter afstand. + Del din placering for at finde apoteker i nærheden af dig. Okay Indtast den aktuelle PIN-kode Forkert pinkode indtastet @@ -643,12 +614,10 @@ Sundhedskort biometri Ikke logget ind - Vi er interesserede i din mening. Brug venligst fem minutter på at udfylde vores undersøgelse. På forhånd mange tak. Advarselsmeddelelse Apotek føjet til favoritter Apotek fjernet fra favoritter Mine apoteker - Adgangskodestyrke meget god Skrivehandling mislykkedes PIN-koden kunne ikke gemmes Rapport @@ -667,7 +636,6 @@ Indløst den %s Indløst lige nu Indløst klokken %s - Ordre:% s Denne recept blev udfyldt som en del af en behandling for dig. Akut servicegebyr Denne recept kan ikke udfyldes på et apotek om natten uden yderligere betaling af et akutgebyr. @@ -681,7 +649,7 @@ Modtage recepter digitalt? Træk skærmen ned for at opdatere. Ingen opskrifter - Log ind for at modtage opskrifter automatisk, eller tilføj en ny opskrift ved hjælp af ⊕ i øverste hjørne. + Tilmeld dig for at modtage opskrifter automatisk. Tilmeld Opskriftsarkiv Måske senere @@ -718,9 +686,9 @@ Klik på displayet for at springe over det værktøjstip, der vises. Hvordan indløser man? Hvordan vil du gerne modtage din medicin? - Indløs direkte - Indløs medicin på stedet - Bestille + Vis kode + Få den scannet på apoteket + Send til apoteket Reserver eller få det leveret Parat Samlingskode @@ -786,7 +754,7 @@ Dette vil slette alle udgiftskvitteringer fra denne enhed og serveren. Modtag omkostningskvitteringer Dine omkostningskvitteringer gemmes også på opskriftsserveren. - Modtaget + Aktiver I alt: %s %s Vælge Dele @@ -835,17 +803,17 @@ Tilknyttet pinkode påkrævet Kan kun indløses i morgen som selvbetaler Kun %s dage tilbage til at indløse som selvbetaler - \nKan stadig indløses som selvbetaler i %s dage\n - Kun gyldig i %s dage - \nGyldig i %s dage tilbage\n - Gælder kun i morgen + Kan stadig indløses som selvbetaler i %s dage + Kun %s dage tilbage at indløse + Kan stadig indløses i %s dage + Kan kun indløses i morgen Der opkræves gebyrer tager forsikring Opskriften(erne) er blevet overført. Opskriften kan ikke behandles. Prøv igen. Du skal muligvis vælge et andet apotek. Opskriften kan ikke behandles. Apoteket melder om en ukendt fejl. Prøv eventuelt et andet apotek. Recepten blev afvist af apoteket. Recepten kan være ugyldig, eller din leveringsadresse eller kontaktoplysninger kan være ugyldige. - Kan ikke indløses. Tjek venligst din internetforbindelse. + Prøv igen, og vælg eventuelt et andet apotek. Hvis fejlen fortsætter, bedes du kontakte support. Opskriften blev overført. Apoteket melder dog om en behandlingsfejl. Kontakt venligst apoteket. Recepten blev afvist af apoteket. Recepten er allerede indløst. Recepten blev afvist af apoteket. Opskriften er blevet slettet. @@ -878,7 +846,6 @@ Aktiver demotilstand Ikke nu Søg - NFC-aktiveret smartphone påkrævet Modtag omkostningskvitteringer digitalt Når omkostningskvitteringerne er aktiveret, finder du dem her, efter du har indløst din recept. Aktiver @@ -886,11 +853,11 @@ Så snart apoteket har deponeret omkostningskvitteringen, vil den fremgå her. Omkostningskvittering Modtag omkostningskvitteringer digitalt - Bemærk: Du vil ikke længere modtage dine omkostningskvitteringer som udskrift i\n Apotek.\n + Bemærk: Du modtager ikke længere dine omkostningskvitteringer som udskrift på apoteket. Aktiver Måske senere aftale - Med dit samtykke vil du undlade at udskrive det på apoteket\n og indsend dine omkostningskvitteringer digitalt i fremtiden.\n + Med dit samtykke vil du undlade at printe dem ud på apoteket og fremsende dine omkostningskvitteringer digitalt fremover. Afbestille Aftalt Afslut demotilstand @@ -905,17 +872,17 @@ Intet internet Der er ingen forbindelse til internettet. Forkert anmodning - Der var et problem med anmodningen. Vi arbejder på en løsning.\n + Der var et problem med anmodningen. Vi arbejder på en løsning. serveren svarer ikke Prøv venligst igen om et par minutter. Forbindelsen fejlede - Ingen information kan hentes i øjeblikket. Vær venlig\n Prøv igen senere.\n + Ingen information kan hentes i øjeblikket. Prøv igen senere. Afvist - Serveren afviste din anmodning. Prøv det venligst\n igen senere.\n + Serveren afviste din anmodning. Prøv igen senere. Opdater app - For at bruge denne funktion skal du opdatere din app.\n + For at bruge denne funktion skal du opdatere din app. Login mislykkedes - Serveren havde et problem med at logge ind. Kontakt venligst\n en gang til.\n + Serveren havde et problem med at logge ind. Log venligst ind igen. Tilmeld Kopiér URL Opret forbindelse til ekstern sygesikringsapp. @@ -941,12 +908,285 @@ Anbefalede Digitalt sundheds-id Vil du justere indstillinger? - 404 side? Aktiver åbningslinks i dine indstillinger for at fortsætte. Åbn Indstillinger Forældet appversion Din version af appen er forældet og understøttes ikke længere. For at fortsætte med at bruge e-recepten skal du opdatere appen. At opdatere - Din CAN er %s cifre lang. + Dit adgangsnummer er %s cifre langt. Din pinkode kan være %s til %s cifre lang. + Saml op + kurer + Forsendelse + NFC-aktiveret smartphone påkrævet + Vi kunne ikke behandle anmodningen om at slette opskriften. Vi arbejder på en løsning ( %s ). + Noget gik galt under sletning af min recept. Jeg modtog fejlkoden %s . Giv mig besked, når sletning er mulig igen. + Yderligere + Forkert anmodning + Rapport + Slet lokalt + Intet internet + Opskriften kunne ikke slettes. Tjek venligst din internetforbindelse, og prøv igen. + Prøv igen + Okay + Log ind igen + Du skal være logget ind for at slette opskriften (401). + Tilmeld + Afbestille + Sletning umulig + Du må ikke slette recepten på nuværende tidspunkt, fordi den er på apoteket, der skal udfyldes, eller det er en afventende direkte opgave (403). + For mange anmodninger + Du har prøvet at slette opskriften for mange gange. Prøv venligst igen senere (429). + Slet opskrift + Ups... + "Der opstod en uventet serverfejl. Prøv venligst igen senere (500)." + App-sikkerhed ikke mulig. Indstil på forhånd biometrisk sikkerhed (f.eks. fingeraftryk) på din enhed. + Biometrisk backup er ikke mulig for denne enhed, da den ikke understøttes af din enhed. + Fare! Din telefon er ikke godt beskyttet. + Dine loginoplysninger gemmes på din telefon. For at beskytte adgangen bruges din foretrukne login-metode. Denne login-metode, fx swipe-mønster, er ikke særlig sikker. Du bruger funktionen \"Gem loginoplysninger\" på eget ansvar. + Hvis du stadig bruger funktionen \"Gem logindata\", vil du fremover kunne se, tilgå, indløse eller slette recepter med e-recept-appen uden sundhedskort og indtastning af pinkode. + Placering ikke fundet + Send til apoteket + Vis kode + Sprog + Sprog + Tysk + Arabisk + Bulgarsk + Tjekkisk + Dansk + Engelsk + Fransk + Hebraisk + Italiensk + Hollandsk + Polere + Rumænsk + Russisk + Tyrkisk + Ukrainsk + Standard + Digitale omkostningskvitteringer er blevet deaktiveret og slettet. + "E-receptforum" + Ingen erstatningsforberedelse mulig + Udskiftningsforberedelse mulig + Vikarforberedelse (Aut idem) + Apoteker er forpligtet til at prioritere udlevering af lægemidler, som patientens sygesikring har indgået en rabataftale om med lægemiddelproducenter. Dette gælder kun ikke, hvis lægen udelukker \"Aut idem\" på recepten, hvilket ikke er tilfældet med din recept. + Din læge har bestemt, at du skal have det ordinerede præparat. Apoteket bør ikke foretage ombytning på baggrund af en rabataftale (\"Aut idem\"). + Tillad skærmbilleder + Hvis du tillader skærmbilleder, vil den sidste side, du åbnede, forblive synlig i baggrunden, når du skifter apps. Om nødvendigt kan dine personlige data være synlige. Vi anbefaler derfor ikke at tillade skærmbilleder. + Give lov til + Afbestille + Skift dit tastatur til klistermærker eller brug emojis til dit profilbillede. + Vælg profilbillede + Hvordan vil du fortsætte? + Vælg foto + kamera + Emoji + Afbestille + Åbn Indstillinger + Brug + Tag et billede + Rediger profilbillede + Antaget for %s minutter siden + Accepteret den %s + Bare accepteret + Accepteret klokken %s + Sundheds-ID + Vælg forsikring + Hvis registreringen med sundheds-id\'et ikke går som forventet, bedes du følge tipsene fra vores hjælpecenter. + Hjælp + Hjælp + Tips til tilmelding til forsikringsappen + Dit forsikringsselskab er ansvarlig for Sundheds-ID. Kontakt dem, hvis du har spørgsmål til tilmelding. Her er nogle afprøvede tips: + Bemærk venligst, at afhængigt af din forsikring kan det være nødvendigt med en separat app. Kontakt dit forsikringsselskab for at finde ud af, hvad det er. + Start kasseapp\'en og log ind der én gang, inden du begynder at registrere dig i e-recept-appen. + Skift mellem tilmelding med Sundheds-ID og sundhedskort kan give problemer. Log derfor først aktivt ud af din profil, før du ændrer login-muligheden. + Står din forsikring ikke på listen, kan du alternativt tilmelde dig dit sundhedskort og den tilhørende pinkode. + Hvis kasseapparat-appen ikke omdirigerer dig tilbage til e-recept-appen, skal du sørge for at rapportere denne fejl til dit forsikringsselskab. + Hvis forsikringsappen ikke kan tilgås, kan det være nyttigt at gå til din smartphones browserindstillinger og tillade, at links åbnes. + Hvis din smartphone kører Android 14, kan det være nyttigt at tillade åbning af links i indstillingerne. + Åbn Indstillinger + det virkede desværre ikke + Prøv igen senere. + Fejlmeddelelser ved indlæsning. + Vælg venligst en emoji eller tekst + Gemme + Log venligst ind for at fortsætte + Yderligere + Opkald + Skrive\nPost + Til apoteket + Apotek + I dag + I morgen + Rute\nher + Slet recept og omkostningskvittering + Sletter du opskriften, slettes den tilhørende omkostningskvittering også. + Beskeder fra alle profiler vises nu sammen + Nye funktioner + Nye funktioner + Vi arbejder konstant på nye funktioner. Del din mening med os og hjælp os med at bygge en bedre app. + Bestillinger til alle + Under Ordrer kan du nu se ordrerne for alle profiler samlet og se dem meget nemmere + + Ingen refusion af omkostninger + Sygeforsikringen dækker som udgangspunkt ikke udgifterne til denne recept. Som patient er du derfor ansvarlig for fuld betaling. Hvorvidt udgifterne refunderes som led i tillægsforsikring eller lovpligtige ydelser, skal kontrolleres individuelt. + Din forsikring dækker ingen omkostninger til denne recept. + + "Din forsikring dækker ikke recepten %s " + "Din forsikring dækker ikke recepterne %s ." + + Forårsaget + Dato + Ulykke + Arbejdsulykke + Industriel sygdom + Nyheder + Ingen nyheder + Du har ingen beskeder endnu. + 🎉 Din ordre er klar til afhentning. Vis venligst denne afhentningskode for at identificere dig selv. + Desværre var beskeden fra dit apotek tom. Kontakt venligst dit apotek. + Apoteket har givet dig et link. + Nyheder + Bestille + Beskeder + Ingen internetforbindelse + For at se de tilsluttede enheder skal du være tilsluttet biometrien. + Recept blev slettet den %s + Slettet + Ingen opskrifter + Du har ingen opskrifter i arkivet. + Dine udgiftskvitteringer er blevet slettet + Omkostningskvittering + Tilmelding nødvendig + Log venligst ind for at se tilsluttede enheder. + Tilmeld + Tilmeld + Opret forbindelse til %s forsikring + Sundhedskort på smartphone + Ikke logget ind + Registreret + opfriske + Du vil modtage adgangslogfiler, hvis du er logget ind på receptservicen. + Tilmelding nødvendig + Log venligst ind med dit sundhedskort og gem dine loginoplysninger med biometri for at se de tilsluttede enheder. + %1$s x morgen + %1$s x frokost + %1$s x Aften + %1$s x Om natten + Medmindre din læge har givet dig andre instruktioner, kan brugsanvisningen forstås som følger: + Din læge har givet dig denne information om at tage medicinen. + Der er ingen information om, hvordan du skal tage medicinen på din recept. + Din læge har bemærket, at du har fået instruktioner om, hvordan du skal tage denne medicin, som ikke er på recepten. Det kan f.eks. stå på din medicinplan. + Ikke specificeret + DJ + Brugsanvisning + " %s - %s" + Påmindelse om omsætning + Tag påmindelser + Oral medicin + Om optagelsespåmindelserne + Morgen + Middag + Eftermiddag + Om aftenen + medicin + Oplysninger om dosering + 1 + %s / %s + 1 dosis + Skift dosis + Afbestille + Medicinpåmindelse + Medicinskema + Tag doseringen efter lægens ordination + Ved %s kl. tag %s x + Vælg datoer + Længere + Ophæve + EN + Ud af + Sporing + Ubegrænset + Individuelt + Første dag + Sidste dag + Tilføj tid + Ingen indtagspåmindelser + Du kan indstille påmindelser om dine recepter + Til + Slukket + Husk mig + Slå batterioptimering fra for denne app. + Hvis du ikke aktiverer denne mulighed, kan påmindelser om medicin virke upålidelige. + Afbestille + Tillade + Brugsanvisning + Ikke specificeret + information + Husk også i batterioptimeringstilstand + Skift dosis + Crowd + form + sluttede + ubegrænset + til %s + Gentage %s + Hvor det er muligt, bruger vi de oplysninger, der er gemt i recepten, til beregningen. + Afbestille + Spare + Indtil slutningen af ​​pakken + Tid + dosis + Se medicin + Har du allerede taget din medicin? + Tag påmindelser + Ingen aktive minder + Du har ingen påmindelser at tage i dag + Vælg startdato + Vælg slutdato + Velkomst + i din e-recept-app + Prøv igen + Indtast adgangskode + Sørg for, at alle, som du kan dele denne enhed med, og som kender dine loginoplysninger, også har adgang til dine opskrifter. + Indtast adgangskode + Indtast venligst adgangskoden for at låse appen op. + adgangskode + Adgangskode + Indtast venligst en adgangskode for at sikre appen. + Vis adgangskode + Gentag adgangskoden + Har du glemt din adgangskode? Slet venligst appen og geninstaller den. Du kan finde ud af hvorfor i vores %s. + Indtast adgangskode + Adgangskoden skal være mindst otte tegn lang + Adgangskodestyrken er ikke tilstrækkelig + Adgangskodestyrke tilstrækkelig + Sikkerhedskopiering af enheden er ikke mulig + Konfigurer venligst en sikkerhedskopi af enheden først. + Ophæve + Indstillinger + Adgangskodestyrke meget god + App sikkerhed + Sikkerhedskopiering af enheden + Vælg mindst én metode til at sikkerhedskopiere appen. + adgangskode + Skift adgangskode + For at slette skal der oprettes forbindelse til receptserveren + Varerne er klar + Varerne har været klar siden %s + Varerne har været klar lige nu + Varerne har været klar i %s minutter + Varerne har været klar siden %s + Deaktiveret, fordi der ikke er indtastet en adgangskode. + Deaktiveret, fordi adgangskoden er for svag. + Deaktiveret, fordi adgangskoderne ikke stemmer overens. + Udforske + Organdonationsregister + Åbent organdonationsregister? + Du vil blive omdirigeret til organdonorregistret. For at se og ændre dine organdonationsdata skal du logge ind der. + Åben + Ophæve + Funktionen er ikke aktiv i demotilstand diff --git a/app/features/src/main/res/values-en/strings.xml b/app/features/src/main/res/values-en/strings.xml index 05c559a1..eb8abb2f 100644 --- a/app/features/src/main/res/values-en/strings.xml +++ b/app/features/src/main/res/values-en/strings.xml @@ -16,8 +16,8 @@ This is not a valid prescription code This prescription code has already been scanned - %s recipe recognized - %s recipes recognized + %s prescription recognized + %s prescriptions recognized Cancel Camera light @@ -26,7 +26,7 @@ Don\'t cancel Let\'s go What you need: - Enter card access number + Enter access number Enter PIN Try again Failed to connect to the server. @@ -55,26 +55,23 @@ Imprint Publisher gematik GmbH\nFriedrichstr. 136\n10117 Berlin, Germany - Managing Director: Dr. Florian Hartge\n Registration court: Berlin-Charlottenburg District Court\n Commercial register number: HRB 96351\n VAT identification number: DE241843684 + Management: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nRegistration court: Berlin-Charlottenburg District Court\nCommercial register number: HRB 96351\nVAT identification number: DE241843684 Responsible for the content - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Contact Note We strive to use gender-sensitive language. If you notice any errors, we would be pleased to hear from you by email. Germany\'s modern platform for digital medicine Write email Open website - Welcome - Start login Unlock Register Cancel - Security Legal information Imprint Privacy policy Terms of Use - Details + Prescription details Mark as redeemed Mark as not redeemed Dosage form @@ -96,8 +93,6 @@ Establishment number Telephone number Email address - Accident at work - Date of accident Accident company or employer number Do you want to permanently delete this prescription? Delete @@ -120,7 +115,6 @@ Open scanner for prescriptions Settings Suppress screenshots - Prevents the display of a preview image when switching apps Do you allow E-Prescription to analyze your usage behavior anonymously? Technical information Security of your prescription data @@ -142,34 +136,26 @@ Anonymous analysis remains disabled %s Thank you for your support! Register - Please identify yourself in order to download prescriptions. - Note to pharmacies: we obtain the contact details for and information about pharmacies from mein-apothekenportal.de provided by the Deutscher Apothekenverband e.V. Have you found an error or would you like to correct any data? + Please identify yourself. + Note for pharmacies: We obtain the contact details and information about pharmacies from mein-apothekenportal.de. Have you discovered an error or would you like to correct data? Find out more - pharmacies + Pharmacies Unfortunately that didn\'t work \uD83D\uDE15 Please try it again. - Enter password - Next Accessibility aids - Zoom - Enables the app to be zoomed in/out by moving fingers together or apart on the screen (pinch-to-zoom). - Password - Secure your data with a password of your choice. - Password + Enable zooming Save - Show password - Repeat password Recommendations: %s Write email The following information about the hardware and operating system you use is transferred when you send an email: Redeem on site only You cannot yet send e-prescriptions to this pharmacy. Open now - courier service - Mail order + Courier service + Shipment Filters Filter - No location available + Share location Agreed Repeated password matches Error 20 10 76631 @@ -179,8 +165,6 @@ %s unsuccessful login attempts were detected. %s unsuccessful login attempts were detected. - Select optimum device security - This may be a fingerprint, swipe pattern or similar Tokens Access token SSO token @@ -188,7 +172,7 @@ no SSO token available copied to the clipboard Click to copy the token to the clipboard - Validity ends today + Only redeemable today Allow No connection to the server Please try again in a few minutes. @@ -250,11 +234,11 @@ What happens if I use the camera function/read prescriptions using the camera? No new prescriptions available - %s new recipe - %s new recipes + %s new prescription + %s new prescriptions Redeemable - In redemption + Is being processed Redeemed Unknown Display access logs @@ -266,7 +250,7 @@ The prescription is currently being processed and cannot be deleted Accept That didn\'t seem to work - We are aware that the connection with the health card has its pitfalls. In the future, registration should also be possible via an already authenticated health insurance app. \n\n We are also working on ensuring that prescriptions can be redeemed digitally without registering. \n\n Did you notice anything during this process that you would like to share with us? Please write to us, we would also be happy to receive very critical feedback. + We are aware that the connection with the health card has its pitfalls. In the future, registration should therefore also be possible via an already authenticated health insurance app.\n\nWe are also working on making it possible to redeem prescriptions digitally without registration.\n\nDid you notice anything during this process that you would like to tell us about? Please write to us, we are also happy to receive very critical feedback. Connection tips Increase the strength of the connection Remove the protective case if necessary. @@ -289,14 +273,14 @@ Scanned on %s Marked as redeemed on %s How would you like to continue? - Order + Send to pharmacy Available soon Reserve now for collection or have it delivered by courier service or shipping Save to order later on Save prescriptions on device - Continue with %s recipe - Continue with %s recipes + Continue with %s prescription + Continue with %s prescriptions Failed to connect medical card The current profile is already connected to a different medical card (health insurance number %s). @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Privacy & Use Next - You received the PIN for your health card from your health insurance company using a secure procedure such as Postident. + You had to actively order your health card PIN from your health insurance company and then received it via a secure procedure such as Postident. No PIN received PIN Check your connection to the Internet and your device\'s time/date setting. - To log in, press “Unlock”. Locked out? Please review your biometric access data on this device. - Forgotten password? Please delete and reinstall the app. Find out why in our %s. Help zone Quantity and unit Active substance @@ -338,10 +320,6 @@ Undo Note Help us make this app better - Enter password - The password needs to be at least eight characters long - Password strength not sufficient - Password strength sufficient Password is visible Password is not visible Biometrics @@ -376,7 +354,7 @@ Cancel Remove Remove this device? - What would you like to remove %s? + Would you like to remove %s? If you remove %s, the connection to the prescription server will be permanently cut off in a maximum of 12 hours. Loading devices... No devices @@ -393,15 +371,15 @@ We have put a few tips together for you to solve the most common problems. Launch connection tips Unlock - card blocked + Card blocked The PIN was entered incorrectly three times. Your card has therefore been blocked for use with a PIN for security reasons. - unlock card + Unlock card Enter PUK With your PIN you have received an 8-digit PUK from your insurance company. Select new PIN You can choose your new PIN yourself (6 to 8 digits). - PIN remembered? - Please make a note of your PIN and keep it in a safe place. + Remember PIN? + Please make a note of your PIN and keep it in a safe place. Cancel OK Cannot unlock @@ -419,7 +397,7 @@ NFC-enabled medical card Medical card PIN Don\'t have an NFC-enabled medical card and PIN yet? - order now + Order now Or: Sign in with your %s. health insurance company app "Your card access number is located in the top right-hand corner on the front of your medical card." @@ -446,9 +424,6 @@ Sent just now Sent at %s o\'clock No longer valid - Log in with app - Select insurance company - Didn\'t find what you were looking for? This list is constantly being expanded. Login with a medical card is already supported by every health insurance company. Feedback from the e-prescription app We look forward to your feedback. Please use the space below and word your comments as precisely as possible: PUK @@ -462,13 +437,13 @@ Note For security reasons, the connection to the prescription server is terminated after 12 hours. To reconnect, you need a health card and PIN for each connection process. Set up biometric protection - Access data cannot be saved. Set up biometric protection (e.g. fingerprint) on your device beforehand. + It is not possible to save access data. Beforehand, set up biometric security (e.g. fingerprint) on your device. Cancel Settings Note Accept Security of your prescription data - \"This app uses the most secure biometric sensor provided by your device to store your access data in the secure area of the device memory. \" + \This app uses the most secure biometric sensor provided by your device to store your credentials in a protected area of the device\'s storage.\ Using biometric protection for your access data means that you can launch this app in future without your medical card and PIN in order to view, retrieve, redeem or delete prescriptions. Please be aware that people with whom you may share this device and whose biometrics may be stored on this device may also have access to your prescriptions. Unfortunately that didn\'t work @@ -481,7 +456,7 @@ Name Insurance Policyholder number - Card access number + CAN Register Log out Save @@ -494,13 +469,12 @@ Search for insurance Connect to the prescription server now? Logged in successfully - connection lost + Connection lost Connect to the prescription server now? No tokens - You will receive a token when you are logged in to the prescription service.\n - Orders + You will receive a token when you are logged in to the prescription service. Select desired PIN - unlock card + Unlock Card Select PIN Repeat PIN The entries differ from each other. @@ -515,31 +489,31 @@ Receive pickup code Message cannot be displayed Please contact your pharmacy ( %s ). - Show shopping cart link + Pharmacy link Show pickup code Show the message %s at %s o\'clock - Recipe sent to %s . The digital redemption process is still unfamiliar for many pharmacies. If you don\'t hear back by tomorrow, we recommend calling to inquire as a precaution. + Prescription sent to %s . Some pharmacies do not yet have a digital response option. If you do not receive a response by tomorrow, please call as a precaution. Order overview New Course Order - Free of charge for the caller. Service times: Mon - Fri 8:00 a.m. - 8:00 p.m. except on national holidays + Free of charge for the caller. Service times: Mon - Fri 8:00 a.m. - 8:00 p.m. except on federal holidays Pharmacy Select desired PIN Desired PIN saved Currently open and near me Filter by … - start search + Search Direct assignment - pharmacies + Pharmacies Telephone number (optional) - Search for name or address + Search No valid pharmacy information No current information was found about this pharmacy. The entry for this pharmacy will be deleted. OK Pharmacy directory not available - Currently no current information about this pharmacy can be accessed. Please check your internet connection. + No information about this pharmacy can be accessed. Please check your internet connection. Cancel Try again Save Environment @@ -554,20 +528,19 @@ Product improvements Anonymous analysis Help us make this app better. All usage data is collected anonymously and is used solely to improve the user experience. - App security - personal settings + Personal settings Accessibility aids Product improvements Added prescription Prescription already imported An error occurred while importing + CAN Delete Scanned prescription - Substitute medication possible Forgotten PIN - %s Recipe - %s Recipes + %s Prescription + %s Prescriptions I have read and accept the privacy policy and terms of use. Privacy policy @@ -579,15 +552,15 @@ You can modify this decision in the system settings at any time Continue Accept - This app uses the safest method provided by your device. + The app uses the safest method you have set up on your device. Save Choose Medicine Trade name Yes No - dosage - date of issue + Dosage + Date of issue This prescription will be redeemed for you as part of a treatment. Not specified Additional payment @@ -595,7 +568,7 @@ Directions for use Eligible according to BVG Alternative preparation - formula name + Prescription name Packaging Manufacturing instructions Description @@ -607,15 +580,13 @@ What is a direct assignment? With direct referral, a prescription from a practice or hospital is filled directly at a pharmacy. Insured persons do not have to take any action and cannot intervene in the redemption process. \n\n Direct referrals are listed in the e-prescription app to make your treatment more transparent for you. Emergency service fee - Sometimes hurry is required. Some prescriptions can be redeemed without the additional payment of an emergency service fee, such as at night or on public holidays. + If a prescription is redeemed between 8 p.m. and 6 a.m. or on Sundays and public holidays, an additional fee of 2.50 euros may be charged. Medications subject to co-payment Exempt from additional payment - Those with statutory health insurance must pay an additional payment of up to ten euros for prescription medication. \n\n The amount of the additional payment depends on the price of your medication. You have to pay for medications that cost less than €5 yourself.\n For medicines that are more expensive, you have to pay ten percent of the price, but at least €5 and a maximum of €10. \n\n Children and young people under the age of 18 are generally exempt from additional payment. \n\n If your annual costs for medication exceed your financial burden limit, you can be exempt from the co-payment. Talk to your health insurance company about this. + People with statutory insurance usually pay a maximum of 10 euros for prescription drugs. Higher fees may apply if a drug from a specific manufacturer is requested that is not covered by a discount contract with the health insurance company (\"desired drug\"). \n\n Children and young people under 18 are exempt from additional payments. \n\n If prescriptions are redeemed later than 28 days after they are issued, the costs must be borne in full by the patient. \n\n If medication expenses are high over the year, an exemption from co-payment can be requested from the health insurance company You are exempt from paying a co-payment for this medication. Your health insurance company will cover the cost of the medication. How long is this prescription valid for? During this period, you can redeem your prescription in any pharmacy with a maximum additional payment of €10. - Substitute medication possible - Due to legal requirements from your health insurance company, you may be given an alternative with the same active ingredient. \n\n Medicines can look and be called different, have different prices and manufacturers, but still contain the same active ingredient. The active ingredient itself and the dosage are crucial for the effect of medicines in the body. Patients often receive a different medication at the pharmacy than the one prescribed by the doctor - provided the medication is comparable. There may be therapeutic and economic reasons for the change. Scanned prescription Prescriptions imported from a hardcopy cannot display personal or medical information for security reasons. \n\n Log in to this app with health card or insurance app to view all the information contained in the prescription. Prescription incorrect @@ -623,17 +594,17 @@ Emergency service fee Dosage according to written instructions Phone - website + Website Email - Sorting by distance not possible. + Share your location to find pharmacies near you. OK Enter current PIN Wrong PIN entered The current PIN of your health card - card blocked + Card blocked Unblock your card in Settings > Unblock card. For security reasons, please enter your current PIN. - Forgotten PIN + Forgot PIN Defective prescription Medicine Something seems to have gone wrong when creating your prescription. Report an error? @@ -643,12 +614,10 @@ Insurance card Biometrics Not logged in - We are interested in your opinion. Please take five minutes to complete our survey. Thank you very much in advance. Warning notice - Pharmacy added to favorites - Removed Pharmacy from Favorites + Pharmacy added to favourites + Removed Pharmacy from Favourites My pharmacies - Password strength very good Write operation not successful PIN could not be saved Report @@ -660,14 +629,13 @@ Password not found There is no password stored on your card. You have been logged out - Sign in again to update your recipes. + Sign in again to update your prescriptions. Active ingredient number - potency and unity + Potency and Unity Redeemed %s minutes ago Redeemed on %s Redeemed just now Redeemed at %s o\'clock - Orders This prescription was redeemed for you as part of a treatment. Emergency service fee This prescription cannot be filled at a pharmacy at night without additional payment of an emergency service fee. @@ -681,13 +649,13 @@ Receive prescriptions digitally? Pull down the screen to refresh. No prescriptions - Sign in to receive recipes automatically or add a new recipe using the ⊕ in the top corner. + Sign up to receive prescriptions automatically. Register - prescription archive + Prescription Archive Maybe later Register Edit profile picture - prescription archive + Prescription Archive Enter name Save My order @@ -706,28 +674,28 @@ To redeem prescriptions, at least one prescription must be selected. Add contact details Change - No recipe + No prescription You currently have no redeemable prescriptions - collection - courier + Pickup + Courier Mail order - choose prescriptions + Choose prescriptions Tap here to scan prescriptions Long press to edit names Add more profiles, eg for your children or parents Click on the display to skip the tooltip that appears. How to redeem? How would you like to receive your medication? - Redeem directly - Redeem medication on site - Order + Show code + Get scanned at the pharmacy + Send to pharmacy Reserve or have it delivered Done Collection code Individual codes - You have %s recipe. - You have %s recipes. + You have %s prescription. + You have %s prescriptions. Make a selection All prescriptions @@ -746,11 +714,11 @@ Who can register for the prescription service? Registration for the prescription service in the digital health network is possible for insured persons, pharmacies, practices and hospitals. Why does the e-prescription app use Google features? - Google offers functions that can be easily integrated into apps and that Google continually develops and updates. This ensures that the functions work on many different devices and can be operated safely. The app uses a feature to improve the camera and scanning functionality for Android devices (Google ML Kit). + Google offers functions that can be easily integrated into apps and that are constantly being developed and updated by Google. This ensures that the functions work on many different end devices and can be operated securely. The app uses a function to improve the camera and scan function for Android devices (Google ML Kit). How does scanning enhancement work with Google ML Kit? Google ML Kit helps to optimize the image captured by a camera so that the prescription codes can be read even in poor lighting conditions or with older camera models. Will data about the prescription or my medication be shared with Google? - no The read prescription code is saved directly in the app. It will not be passed on to Google. The prescription data is not stored in the code, only in the digital health network. From there they are sent to the app. Google does not have access to the digital health network. + No. The read prescription code is saved directly in the app. It will not be passed on to Google. The prescription data is not stored in the code, only in the digital health network. From there they are sent to the app. Google does not have access to the digital health network. What data does Google process when using ML Kit? Google only has access to technical information about the end device used and the general use of the additional function (e.g. error rate, camera settings) in order to record this statistically and thus improve the additional function. When you access, Google temporarily records the IP address of your end device. Information about you and the contents of the prescription will not be recorded by Google. Is the use of Google ML Kit voluntary? @@ -774,9 +742,9 @@ PIN entered incorrectly. Access number entered incorrectly PUK entered incorrectly. - cost receipts + Cost receipts View cost receipts - cost receipts + Cost receipts To receive cost receipts, you must be connected to the server. Connect No cost receipts @@ -785,8 +753,8 @@ Deactivate function This will delete all expense receipts from this device and the server. Receive cost receipts - Your cost receipts are also saved on the recipe server. - Received + Your cost receipts are also saved on the prescription server. + Enable Total: %s %s Select Split @@ -794,7 +762,7 @@ Delete Submit %s € - total price + Total price Tip: Submit cost receipts via the insurance app Submit cost receipts easily via your insurance company’s app. In the next step, select this app and press share. Practice @@ -819,13 +787,13 @@ BTM fee T-prescription fee Procurement costs - courier service + Courier Total in EUR: %s - levy - Really delete? + Duty + For sure, delete? The file will be deleted from your device and the server. Delete - Posted + Sent Postcode Place Will be redeemed for you @@ -835,29 +803,29 @@ Associated PIN required Can only be redeemed tomorrow as a self-payer Only %s days left to redeem as self-payer - \nStill redeemable as a self-payer for %s days\n - Valid for %s days only - \nValid for %s days left\n - Only valid tomorrow + Still, redeemable as a self-payer for %s days + Only %s days left to redeem + Still redeemable for %s days + Only redeemable tomorrow Charges apply Takes insurance - The recipe(s) have been successfully transferred. - The recipe cannot be processed. Please try again. You may need to choose a different pharmacy. - The recipe cannot be processed. The pharmacy reports an unknown error. If necessary, try another pharmacy. + The prescription(s) have been successfully transferred. + The prescription cannot be processed. Please try again. You may need to choose a different pharmacy. + The prescription cannot be processed. The pharmacy reports an unknown error. If necessary, try another pharmacy. The prescription was rejected by the pharmacy. The prescription may be invalid or your delivery address or contact information may be invalid. - Unable to redeem, please check your internet connection. - The recipe was successfully transferred. However, the pharmacy reports a processing error. Please contact the pharmacy. + Try again and possibly choose a different pharmacy. If the error persists, please contact support. + The prescription was successfully transferred. However, the pharmacy reports a processing error. Please contact the pharmacy. The prescription was rejected by the pharmacy. The prescription has already been redeemed. - The prescription was rejected by the pharmacy. The recipe has been deleted. - The recipe could not be transferred. Please check your internet connection and try again. - One or more recipes could not be transferred. + The prescription was rejected by the pharmacy. The prescription has been deleted. + The prescription could not be transferred. Please check your internet connection and try again. + One or more prescriptions could not be transferred. Error sending Shipped successfully! Error at the pharmacy Error at the pharmacy Contact pharmacy Prescription already redeemed - Recipe deleted + Prescription deleted No Internet To receive access logs, you must be connected to the server. You can still fill the prescription at a pharmacy within this period, but you will have to pay the entire purchase price for the medication yourself. Alternatively, you can ask your practice to have the prescription reissued. @@ -878,7 +846,6 @@ Activate demo mode Not now Search - NFC-enabled smartphone required Receive cost receipts digitally Once the cost receipts have been activated, you will find them here after redeeming your prescription. Enable @@ -886,11 +853,11 @@ As soon as the pharmacy has deposited the cost receipt, it will appear here. Cost receipt Receive cost receipts digitally - Note: You will no longer receive your cost receipts as a printout in the\n Pharmacy.\n + Note: You will no longer receive your cost receipts as a printout at the pharmacy. Enable Maybe later - agreement - With your consent, you will refrain from printing it in the pharmacy\n and submit your cost receipts digitally in the future.\n + Agreement + With your consent, you will refrain from printing them out at the pharmacy and will submit your cost receipts digitally in the future. Cancel Agreed Exit demo mode @@ -905,17 +872,17 @@ No Internet There is no connection to the Internet. Incorrect request - There was a problem with the request. We work on a solution.\n + There was a problem with the request. We work on a solution. Server not responding Please try again in a few minutes. Failed to connect - No information can currently be retrieved. Please\n try again later.\n + No information can currently be retrieved. Please try again later. Rejected - The server rejected your request. Please try it\n again later.\n + The server rejected your request. Please try again later. Update app - To use this feature, please update your app.\n + To use this feature, please update your app. Login failed - The server had a problem logging in. Please get in touch\n again.\n + The server had a problem logging in. Please sign in again. Register Copy URL Connect to external health insurance app. @@ -925,7 +892,7 @@ Please enter a telephone number for contact purposes. Please enter a first name and surname for contact purposes. Please enter a street and house number for contact purposes. - Please provide your zip code to contact us. + Please provide your postcode to contact us. Please indicate your place of residence to contact us. The phone number you entered is invalid. (too short, too long, invalid characters) Please enter an email address to contact us @@ -941,12 +908,285 @@ Recommended Digital health ID Adjust settings? - 404 page? Please enable opening links in your settings to continue. Open settings Outdated app version Your version of the app is outdated and no longer supported. To continue using the e-prescription, please update the app. Update - Your CAN is %s digits long. + Your access number is %s digits long. Your PIN can be %s to %s digits long. + Pick up + Courier + Shipment + NFC-enabled smartphone required + We were unable to process the request to delete the prescription. We are working on a solution ( %s ). + Something went wrong while deleting my prescription, I received the error code %s . Please notify me when deletion is possible again. + Next + Incorrect request + Report + Delete locally + No Internet + The prescription could not be deleted. Please check your internet connection and try again. + Try again + OK + Re-login + You must be logged in to delete the prescription (401). + Login + Cancel + Deletion impossible + You are not allowed to delete the prescription at this time because it is at the pharmacy to be filled or it is a pending direct assignment (403). + Too many requests + You\'ve tried deleting the prescription too many times. Please try again later (429). + Delete prescription + Oops... + "An unexpected server error occurred. Please try again later (500)." + App security not possible. Beforehand, set up biometric security (e.g. fingerprint, faceID) on your device. + Biometric backup is not possible as it is not supported by your device. + Attention! Your phone is not well protected. + Your login details are saved on your phone. To protect access, your preferred login method is used. This login method, e.g. swipe pattern, is not very secure. You use the “Save login details” feature at your own risk. + If you still use the \"Save login data\" function, you will be able to view, access, redeem or delete prescriptions with the e-prescription app in the future without a health card and entering the PIN. + Location not found + Send to Pharmacy + Show Code + Language + Language + German + Arabic + Bulgarian + Czech + Danish + English + French + Hebrew + Italian + Dutch + Polish + Romanian + Russian + Turkish + Ukrainian + Default + Digital cost receipts have been deactivated and deleted. + "Forum for e-prescriptions" + No replacement product possible + Substitute medication possible + Replacement drug (Aut idem) + Pharmacists are obliged to give priority to dispensing medicines for which the patient\'s health insurance company has concluded a discount agreement with drug manufacturers. This only does not apply if the doctor excludes \"Aut idem\" on the prescription, which is not the case with your prescription. + Your doctor has determined that you should receive the prescribed medication. The pharmacy should not make any exchanges based on a discount agreement (“Aut idem”). + Allow screenshots + If you allow screenshots, the last page you opened will remain visible in the background when you switch apps. Your personal data may be visible as a result. We therefore recommend that you do not allow screenshots. + Allow + Cancel + Switch your keyboard to stickers or use emojis for your profile picture. + Choose profile picture + How would you like to continue? + Select photo + Camera + Emoji + Cancel + Open settings + Use + Capture + Edit profile picture + Accepted %s minutes ago + Accepted on %s + Just accepted + Accepted at %s o\'clock + Health ID + Select insurance company + If registration with the Health ID does not work as expected, please follow the tips in our help. + Help + Help + Tips for registering with the insurance app + Your insurance company is responsible for the health ID. Please contact them if you have any questions about registration. Here are some tried and tested tips: + Please note that depending on your insurance, a separate app is required. Please ask your insurance company which one this is. + Start the Insurance app and log in there once before you start logging in to the e-prescription app. + Switching between logging in with your health ID and your health card can cause problems. Please first actively log out of your profile before changing the login option. + If your insurance is not on the list, you can alternatively log in with your health card and the corresponding PIN. + If you are not redirected back to the e-prescription app by the health insurer\'s app, please be sure to report this error to your insurer. + If the insurance app cannot be accessed, it can be helpful to go to the browser settings of your smartphone and allow links to be opened. + If your smartphone is running Android 14, it might be helpful to allow opening links in the settings. + Open Settings + Unfortunately, that didn\'t work out + Please try again at a later time. + Error messages when loading. + Please select an emoji or text + Save + Please log in to continue + Next + Call + Write \nMail + To the pharmacy + Pharmacy + Today + Tomorrow + Route\nhere + Delete prescription and cost receipt + If you delete the prescription, the associated expense receipt will also be deleted. + The messages from all profiles are now displayed together + New Features + New Features + We are constantly working on new features. Let us know what you think and help us make a better app. + Orders for everyone + Under Orders, you can now see the orders for all profiles together and view them much more easily + + No reimbursement of costs + As a rule, health insurance does not cover the costs of this prescription. As a patient, you are therefore responsible for paying the full amount. Whether the costs will be reimbursed as part of supplementary insurance or statutory benefits must be checked individually. + Your insurance will not cover the cost of this prescription. + + "Your insurance does not cover the prescription %s " + "Your insurance does not cover the prescriptions %s ." + + Caused + Date + Accident + Accident at work + Occupational illness + Messages + No messages + You don\'t have any messages yet. + 🎉 Your order is ready for collection. Please show this pickup code to identify yourself. + Unfortunately, your pharmacy\'s message was empty. Please contact your pharmacy. + The pharmacy has provided you with a link. + Messages + Order + Messages + No internet connection + To display the connected devices, you must be connected via biometrics. + Prescription was deleted on %s + Deleted + No prescriptions + You have no prescriptions in the archive. + Your cost receipts have been deleted + Cost receipt + Registration necessary + Please log in to view connected devices. + Log in + Log in + Connecting to %s Insurance + Health card on your smartphone + Not logged in + Logged in + refresh + You will receive access logs when you are logged in to the prescription service. + Registration required + Please log in with your health card and save your login details with biometrics to view the connected devices. + %1$s x Morning + %1$s x noon + %1$s x evening + %1$s x Night + Unless your doctor instructs you otherwise, the instructions for use can be understood as follows: + Your doctor has given you this information about how to take your medicine. + Your prescription does not contain any information regarding dosage instructions. + Your doctor has noted that you have been given instructions on how to take your medicine that are not on the prescription. For example, this may be on your medication plan. + Not specified + DJ + dosage instructions + " %s - %s" + medication reminder + Medication reminders + oral medications + To the medication reminders + morning + lunchtime + afternoon + At evening + Medicine + dosage information + 1 + %s / %s + 1 dose + change dosage + Cancel + medication reminder + medication plan + Take the dosage as prescribed by your doctor + At %s o\'clock %s take x + Select dates + Next + Cancel + On + Off + Tracking + Unlimited + Individually + First Day + Last Day + Add time + No medication reminders + You can set reminders for your prescriptions + On + Off + Remind me + Turn off battery optimization for this app. + If you do not activate this option, medication reminders may be displayed unreliably. + Cancel + Allow + dosage instructions + Not specified + information + Also remember in battery optimization mode + change dosage + Quantity + form + finished + unlimited + to %s + Repeat %s + Where possible, we use the information stored in the prescription for the calculation. + Cancel + Save + Until the end of the pack + Time + dosage + Show medications + Have you already taken your medication? + Medication reminders + No active reminders + You have no reminders to take your medication today + Select start date + Select end date + Welcome + in your e-prescription app + Try again + Enter password + Please ensure that anyone with whom you share this device and who knows your login information also has access to your recipes. + Enter password + Please enter the password to unlock the app. + password + Password + Please enter a password to secure the app. + Show password + Repeat password + Forgot your password? Please delete the app and then reinstall it. You can find out why in our %s. + Enter password + The password must be at least eight characters long + Password strength not sufficient + password strength sufficient + Device backup not possible + Please set up a device backup first. + Cancel + Settings + password strength very good + app security + Device security + Please select at least one method to back up the app. + password + Change Password + To delete, a connection to the Prescription Server must be established + goods are ready + Goods have been ready since %s + Goods are ready since just now + Goods have been ready for %s minutes + Goods have been ready since %s + Disabled because no password is entered. + Disabled because the password is too weak. + Disabled because the passwords do not match. + Explore + organ donation register + Open organ donation register? + You will be redirected to the organ donation register. In order to view and change your organ donation data, you must log in there. + Open + Cancel + Function not active in demo mode diff --git a/app/features/src/main/res/values-es/strings.xml b/app/features/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..39d52294 --- /dev/null +++ b/app/features/src/main/res/values-es/strings.xml @@ -0,0 +1,1192 @@ + + + DE ACUERDO + Cancelar + Atrás + en + Digital. Rápido. Seguro. + Identificación de la tarea + Código de acceso + Condiciones de uso + Política de privacidad + Recetas + Acceso a la cámara denegado + Para utilizar el escáner, debe permitir que la aplicación acceda a su cámara en la configuración del sistema. + Enfoque la cámara en un código de receta + Este no es un código de receta válido + Este código de receta ya ha sido escaneado + + %s receta reconocida + %s recetas reconocidas + + Cancelar + Luz de la cámara + ¿Cancelar el escaneo? + DE ACUERDO + No canceles + Vamos + Lo que necesitas: + Introduzca el número de acceso + Introducir PIN + Intentar otra vez + No se pudo conectar al servidor. + + Tienes %s un intento más antes de que tu tarjeta quede bloqueada para el uso del PIN. + Tienes %s intentos más antes de que tu tarjeta quede bloqueada para el uso del PIN. + + Encontrará el número de acceso en la esquina superior derecha de su tarjeta médica. + Cancelar + Buscando tarjeta... + Coloque la tarjeta médica en la parte posterior del dispositivo. + Todavía buscando... + Mueva lentamente la tarjeta en la parte posterior del dispositivo. + Consejo + Las carcasas de los dispositivos pueden dificultar la conexión a través de NFC. + Tarjeta reconocida + Procure no mover la tarjeta médica. + Se encontró la tarjeta médica. No se mueva. + Conexión interrumpida + Vuelva a colocar su tarjeta médica en la parte posterior del dispositivo. + Versión: %s + Hash de construcción: %s + Menú de depuración + Abierto hasta %s en punto + Abierto continuamente + Imprimir + Editor + gematik GmbH\nFriedrichstr. 136\n10117 Berlín, Alemania + Dirección: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nTribunal de registro: Tribunal de distrito de Berlín-Charlottenburg\nNúmero de registro mercantil: HRB 96351\nNúmero de identificación del IVA: DE241843684 + Responsable del contenido + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge + Contacto + Nota + Nos esforzamos por utilizar un lenguaje que tenga en cuenta las cuestiones de género. Si detecta algún error, nos complacerá comunicárselo por correo electrónico. + La moderna plataforma alemana para la medicina digital + Escribir correo electrónico + Abrir sitio web + Descubrir + Registro + Cancelar + Información legal + Imprimir + Política de privacidad + Condiciones de uso + Detalles de la prescripción + Marcar como redimido + Marcar como no redimido + Forma de dosificación + Tamaño del paquete + Persona asegurada + Nombre + DIRECCIÓN + Fecha de nacimiento + Seguro de salud / unidad de costo + Estado + Número de asegurado + Prescriptor + Nombre + Médico especialista + Número de médico (LANR) + Institución + Nombre + DIRECCIÓN + Número de establecimiento + Número telefónico + Dirección de correo electrónico + Número de empresa o empleador del accidente + ¿Quieres eliminar permanentemente esta receta? + Borrar + Cancelar + Horario de apertura + Sitio web + Solo se puede canjear hoy como cliente que paga por cuenta propia + Registro + Habilitar NFC + Habilite la función NFC en su dispositivo para iniciar sesión con su tarjeta médica. + Permitir + Correcto + ¿Recetas canjeadas? + ¿Quieres marcar las recetas como canjeadas? + No redimido + Redimido + Abre a %s en punto + +49 800 277 377 7 + Línea telefónica directa + Escáner abierto para recetas + Ajustes + Suprimir capturas de pantalla + ¿Permite que E-Prescription analice su comportamiento de uso de forma anónima? + Información técnica + Seguridad de los datos de sus recetas + Tenga en cuenta que las personas con quienes pueda compartir este dispositivo y cuyos datos biométricos puedan estar almacenados en este dispositivo también podrán tener acceso a sus recetas. + Envío fallido + No hay ningún programa de correo electrónico configurado + No hay resultados + No pudimos encontrar ningún resultado con este término de búsqueda. + Licencias de código abierto + Contacto + Llamar al soporte telefónico + Encuesta sobre la aplicación + +49 800 277 377 7 + Me gustaría ayudar a mejorar la aplicación. + Esto incluye la información de hardware y software de su teléfono, la configuración de la aplicación de receta electrónica y el alcance de uso, pero nunca sus datos personales o de salud. + Los datos son proporcionados exclusivamente por los proveedores de tratamiento de datos a gematik GmbH y se eliminan después de un máximo de 180 días. Puede desactivar el análisis en cualquier momento a través del menú de la aplicación. + Estos datos nos permiten entender qué funciones se utilizan con más frecuencia y mejorarlas. También podemos estimar durante cuánto tiempo se debe mantener la tecnología antigua y cuándo podemos, por ejemplo, hacer obligatoria una versión más nueva del sistema operativo sin afectar a (demasiados) usuarios. + Mejorar la aplicación + El análisis anónimo permanece deshabilitado + %s ¡Gracias por tu apoyo! + Registro + Por favor, identifíquese. + Nota para farmacias: Los datos de contacto y la información sobre farmacias los obtenemos de mein-apothekenportal.de. ¿Ha detectado algún error o desea corregir los datos? + Saber más + Farmacias + Lamentablemente eso no funcionó \uD83D\uDE15 + Por favor, inténtalo de nuevo. + Ayudas de accesibilidad + Habilitar zoom + Ahorrar + Recomendaciones: %s + Escribir correo electrónico + La siguiente información sobre el hardware y el sistema operativo que utiliza se transfiere cuando envía un correo electrónico: + Canjear solo en el sitio + Todavía no es posible enviar recetas electrónicas a esta farmacia. + Abierto ahora + Servicio de mensajería + Envío + Filtros + Filtrar + Compartir ubicación + Acordado + Coincidencias de contraseñas repetidas + Error 20 10 76631 + El certificado de su tarjeta sanitaria no es válido. ¿Su tarjeta ha caducado? Póngase en contacto con su compañía de seguros médicos. + Intentos de inicio de sesión fallidos + + Se detectaron %s intentos de inicio de sesión fallidos. + Se detectaron %s intentos de inicio de sesión fallidos. + + Fichas + Token de acceso + Token SSO + No hay token de acceso disponible + No hay token SSO disponible + copiado al portapapeles + Haga clic para copiar el token al portapapeles + Solo canjeable hoy + Permitir + No hay conexión con el servidor + Por favor, inténtelo de nuevo en unos minutos. + Recargar + Mostrar tokens + ¿Cómo le gustaría proteger esta aplicación? + Nota + No se ha configurado ninguna seguridad para este dispositivo + Le recomendamos que agregue protección adicional a sus datos médicos asegurando su dispositivo, por ejemplo, con un código o datos biométricos. + No mostrar este mensaje en el futuro. + La conexión falló. No se pudo crear una conexión de red. + Falló la comunicación con el servidor: código de estado %s. + No se pudo comunicar con el servidor: verifique la conexión a Internet y la configuración de hora/fecha. + Advertencia + Es posible que su dispositivo tenga seguridad reducida + Esto puede deberse, por ejemplo, a dispositivos manipulados o a que el modo de desarrollador esté activado. Recomendamos no utilizar la aplicación en dispositivos con jailbreak por motivos de seguridad. + Reconozco el mayor riesgo y me gustaría continuar de todos modos. + ¿Por qué los dispositivos con acceso root son un riesgo potencial para la seguridad? + Saber más + Nombre del perfil + Introduzca un nombre para el nuevo perfil. + Nombre del perfil + Perfiles + Cómo identificar una tarjeta médica con tecnología NFC + No es posible contactar a través de esta aplicación + Por favor, póngase en contacto con su compañía de seguros de salud a través de los canales habituales. + Tarjeta médica y PIN + Solo PIN + Iniciar sesión en la aplicación de receta electrónica + El campo de nombre no puede estar vacío. + Ya existe un perfil con este nombre. + Perfil + %s seleccionado + Color de fondo + Gris primaveral + Rocío del sol + ¡Es! ¡Rosa! + Árbol + Luna azul de septiembre + No ha iniciado sesión + Conectado + Última conexión el %s + ¿Borrar perfil? + Esto eliminará todos los datos del perfil de este dispositivo. Se conservarán sus recetas en la red de salud. + Borrar + Cancelar + Eliminar perfil + Desea eliminar el último perfil. + La aplicación requiere al menos un perfil. Ingrese un nombre para el nuevo perfil. + Error 20 10 76831 + No se ha podido acceder al registro de tarjetas médicas. Inténtelo nuevamente. + En el Portal Nacional de Salud podrá encontrar información verificada profesionalmente sobre enfermedades, códigos CIE y temas relacionados con la prevención y la atención sanitaria. + Abrir gesund.bund.de + Hemos modificado la Política de privacidad + La aplicación de receta electrónica ha evolucionado, por lo que hemos tenido que actualizar nuestra Política de Privacidad. + Abrir política de privacidad + Esto ha cambiado desde %s : + ¿Qué pasa cuando abres la aplicación? + ¿Qué sucede si uso la función de cámara/leo recetas usando la cámara? + No hay nuevas recetas disponibles + + %s nueva receta + %s nuevas recetas + + Redimible + Se está procesando + Redimido + Desconocido + Mostrar registros de acceso + ¿Quién accedió a sus recetas y cuándo? + Clave de acceso al servicio de prescripción + Registros de acceso + Sin registros de acceso + Todavía no hay registros de acceso disponibles. + La receta se está procesando actualmente y no se puede eliminar. + Aceptar + Eso no pareció funcionar + Somos conscientes de que la conexión con la tarjeta sanitaria tiene sus inconvenientes. En el futuro, el registro también debería ser posible a través de una aplicación de seguro médico ya autenticada.\n\nTambién estamos trabajando para garantizar que las recetas se puedan canjear digitalmente sin registrarse.\n\n¿Notaste algo durante este proceso que te gustaría compartir con nosotros? Escríbanos, también estaremos encantados de recibir comentarios muy críticos. + Consejos de conexión + Aumentar la fuerza de la conexión + Si es necesario, retire la funda protectora. + Si el dispositivo vibra y luego interrumpe la conexión, busque la posición ideal dentro de un radio pequeño. + Mueva el dispositivo únicamente muy lentamente sobre la tarjeta. + Coloque el dispositivo directamente sobre la tarjeta. + Puede hacerlo colocando la tarjeta médica sobre una superficie plana (por ejemplo, una mesa). + Aumentar la fuerza de la conexión + Tenga en cuenta la posición del sensor NFC + Descubra dónde se encuentra el sensor NFC en su dispositivo (por ejemplo, esta es una descripción general de los dispositivos por %s ). + En algunos casos, la posición del sensor NFC puede variar dentro de una serie de modelos (estos son los detalles, por ejemplo, para %s ). + Siguiente consejo + Próximo + Cerca + Probar + Contáctenos + Búsqueda de farmacias con licencia + Canjear + Receta escaneada + Escaneado %s + Marcado como canjeado el %s + ¿Cómo te gustaría continuar? + Enviar a farmacia + Disponible próximamente + Reserva ahora para recogerlo o recíbelo por mensajería o envío. + Guardar para ordenar más tarde + Guardar recetas en el dispositivo + + Continuar con la prescripción %s + Continuar con %s prescripciones + + No se pudo conectar la tarjeta médica + El perfil actual ya está conectado a una tarjeta médica diferente (número de seguro médico %s ). + Su tarjeta médica ya está vinculada a un perfil diferente. Cambie al perfil %s. + Ahorrar + Datos de contacto y dirección + Contacto + Número telefónico + Dirección de correo electrónico (opcional) + Dirección de entrega + Nombre y apellido + Calle y numero de casa + Línea de dirección adicional (opcional) + Instrucciones de entrega (opcional) + Se requieren más datos de contacto + ¿Descartar cambios? + Desechar + Las búsquedas en el directorio de farmacias se realizan mediante coordenadas geográficas proporcionadas con la ayuda de OpenStreetMap. Agradecemos a este proyecto su ayuda. + © OpenStreetMap ( %s ) + https://www.openstreetmap.org/copyright + Privacidad y uso + Próximo + Tenías que solicitar activamente el PIN de tu tarjeta sanitaria a tu compañía de seguro médico y luego recibirlo a través de un proceso seguro como Postident. + No se recibió ningún PIN + ALFILER + Verifique su conexión a Internet y la configuración de fecha y hora de su dispositivo. + ¿Bloqueado? Revise sus datos de acceso biométrico en este dispositivo. + Zona de ayuda + Cantidad y unidad + Sustancia activa + Cantidad de sustancia activa + Descripción del lote + Uso por + Categoría + Vacuna + Aceptar + Deshacer + Nota + Ayúdanos a mejorar esta aplicación + La contraseña es visible + La contraseña no es visible + Biometría + Contraseña + Esperando respuesta + Sin prescripciones + Actualmente no tienes ninguna receta que puedas canjear. + Actualizar + Cierre de sesión automático + Por razones de seguridad, se desconectará del servidor de recetas durante 12 horas. Vuelva a conectarse para recuperar las recetas actuales. + Conectar + ¿Has recibido una impresión en papel? + Agregue recetas a su lista tocando el botón de escaneo en la esquina superior derecha. + Impresión en papel escaneado + Debe iniciar sesión para recibir recetas automáticamente. + Registro + No hay recetas canjeadas + Aquí se muestran las recetas que ha canjeado. Sus recetas se eliminarán del servidor de recetas después de 100 días por motivos de protección de datos. + No hay recetas canjeadas + Aquí se muestran las recetas que ha canjeado. Escanee las recetas nuevas para iniciar el proceso de canje. + Gestión de dispositivos + Dispositivos conectados + Registrado desde %s (este dispositivo) + Registrado desde %s + Por razones de seguridad, la conexión con el servidor de recetas se interrumpe al cabo de 12 horas. Para volver a conectarse, es necesario disponer de la tarjeta sanitaria y del PIN correspondiente para cada proceso de conexión. + ALFILER + Introduzca su PIN (tarjeta médica). + Próximo + Registro + Dispositivos conectados + ¿Quitar dispositivo? + Cancelar + Eliminar + ¿Quitar este dispositivo? + ¿Quieres eliminar %s? + Si elimina %s, la conexión con el servidor de recetas se cortará permanentemente en un máximo de 12 horas. + Cargando dispositivos... + Sin dispositivos + No hay dispositivos asociados a esta tarjeta médica. + Intentar otra vez + Uh oh :-( + No se pudo cargar la lista de dispositivos. + Sin conexión + Sin conexión a Internet. + Medicamentos y apósitos + Narcóticos + Expedición de medicamentos sujetos a prescripción médica de conformidad con el artículo 4 AMVV + ¿Necesitas ayuda? + Hemos reunido algunos consejos para que puedas solucionar los problemas más comunes. + Consejos para la conexión de lanzamiento + Descubrir + Tarjeta bloqueada + El PIN se ha introducido incorrectamente tres veces. Por tanto, su tarjeta ha sido bloqueada para su uso con PIN por motivos de seguridad. + Desbloquear tarjeta + Ingresar PUK + Con su PIN ha recibido un PUK de 8 dígitos de su compañía de seguros. + Seleccionar nuevo PIN + Usted mismo podrá elegir su nuevo PIN (de 6 a 8 dígitos). + ¿Recordar PIN? + Por favor, anote su PIN y guárdelo en un lugar seguro. + Cancelar + DE ACUERDO + No se puede desbloquear + Has utilizado este PUK para desbloquear tu tarjeta el número máximo de veces o lo has introducido incorrectamente en repetidas ocasiones. Ponte en contacto con tu compañía de seguros médicos. + Puedes usar un PUK para hasta 10 desbloqueos. + tarjeta desbloqueada + Lo que necesitas: + Su tarjeta médica + Tarjeta médica PUK + Próximo + Tarjeta de seguro + Pedir PIN o tarjeta + Registro + ¿Cómo le gustaría autenticarse? + Tarjeta médica con tecnología NFC + PIN de tarjeta médica + ¿Aún no tienes una tarjeta médica y un PIN con NFC habilitado? + Ordene ahora + O bien: Inicia sesión con tu %s. + aplicación de la compañía de seguros de salud + \"El número de acceso a su tarjeta se encuentra en la esquina superior derecha del frente de su tarjeta médica\". + Mi tarjeta no tiene número de acceso + + Tienes %s un intento más antes de que tu tarjeta quede bloqueada para el uso del PIN. + Tienes %s intentos más antes de que tu tarjeta quede bloqueada para el uso del PIN. + + Coloque la tarjeta médica en la parte posterior del teléfono. + El siguiente proceso puede tardar hasta 30 segundos. + Coloque la tarjeta %s en la parte posterior del teléfono. + En la zona superior derecha + en la zona central superior + En la zona superior izquierda + en la zona central derecha + centralmente + en la zona central izquierda + En la zona inferior derecha + en la zona central inferior + En la zona inferior izquierda + Ayuda + Enviado hace %s minutos + Enviado %s + Enviado ahora mismo + Enviado a %s en punto + Ya no es válido + Comentarios de la aplicación de receta electrónica + Esperamos recibir sus comentarios. Utilice el espacio que aparece a continuación y redacte sus comentarios con la mayor precisión posible: + PUK + Cerca + Qué lástima... + Lamentablemente, su dispositivo no cumple con los requisitos mínimos para iniciar sesión en la aplicación de receta electrónica. Para una autenticación segura con su tarjeta médica, se requiere al menos Android 7 y un chip NFC. + Saber más + ¿Guardar datos de inicio de sesión? + Ahorrar + No guardar + Nota + Por razones de seguridad, la conexión con el servidor de recetas se interrumpe al cabo de 12 horas. Para volver a conectarse, es necesario disponer de la tarjeta sanitaria y del PIN correspondiente para cada proceso de conexión. + Configurar protección biométrica + No es posible guardar los datos de acceso. Configure previamente la seguridad biométrica (por ejemplo, la huella dactilar) en su dispositivo. + Cancelar + Ajustes + Nota + Aceptar + Seguridad de los datos de sus recetas + \Esta aplicación utiliza el sensor biométrico más seguro proporcionado por su dispositivo para proteger sus credenciales en un área protegida del almacenamiento del dispositivo.\ + El uso de protección biométrica para sus datos de acceso significa que en el futuro podrá iniciar esta aplicación sin su tarjeta médica ni PIN para ver, recuperar, canjear o eliminar recetas. + Tenga en cuenta que las personas con quienes pueda compartir este dispositivo y cuyos datos biométricos puedan estar almacenados en este dispositivo también podrán tener acceso a sus recetas. + Lamentablemente eso no funcionó. + La autenticación con la aplicación de la compañía de seguros de salud no fue exitosa. + Expirado el %s + La receta ya ha sido eliminada del servidor. + Corrija su entrada o descarte los cambios + Correcto + Datos del asegurado + Nombre + Seguro + Número de asegurado + PODER + Registro + Finalizar la sesión + Ahorrar + Cambiar + Editar foto de perfil + Próximo + El servidor no responde + Por favor, inténtelo de nuevo más tarde. + Intentar otra vez + Búsqueda de seguros + ¿Conectarse al servidor de recetas ahora? + Inició sesión correctamente + Conexión perdida + ¿Conectarse al servidor de recetas ahora? + Sin fichas + Recibirás un token cuando inicies sesión en el servicio de recetas. + Seleccione el PIN deseado + Desbloquear tarjeta + Seleccionar PIN + Repetir PIN + Las entradas difieren entre sí. + No hay pedidos + Aún no tienes ningún pedido + En este momento + A %s en punto + El carrito de compras está listo + La receta se ha añadido a su cesta de la compra. Por favor, acceda al sitio web de la farmacia para completar el pedido. + Abrir carrito de compras + Muestra este código de colección en la farmacia. + Recibir código de recogida + No se puede mostrar el mensaje + Por favor, póngase en contacto con su farmacia ( %s ). + Enlace de farmacia + Mostrar código de recogida + Mostrar el mensaje + %s a %s en punto + Receta enviada a %s . Algunas farmacias aún no tienen la opción de respuesta digital. Si no recibe una respuesta para mañana, llame por precaución. + Descripción general del pedido + Nuevo + Curso + Orden + Gratuito para quien llama. Horario de atención: de lunes a viernes de 8:00 a 20:00 horas, excepto los días festivos federales. + Farmacia + Seleccione el PIN deseado + PIN deseado guardado + Actualmente abierto y cerca de mí + Filtrar por… + Buscar + Asignación directa + Farmacias + Número de teléfono (opcional) + Buscar + No hay información de farmacia válida + No se ha encontrado información actualizada sobre esta farmacia. La entrada de esta farmacia será eliminada. + DE ACUERDO + Directorio de farmacias no disponible + No se puede acceder a la información sobre esta farmacia. Por favor, compruebe su conexión a Internet. + Cancelar + Intentar otra vez + Salvar el medio ambiente + No es posible iniciar sesión + Parece que tus datos biométricos de acceso han cambiado. Vuelve a iniciar sesión con tu tarjeta sanitaria. + Cancelar + Registro + Perfil 1 + Cerca de mi + Canjeable más tarde + Canjeable desde %s + Mejoras del producto + Análisis anónimo + Ayúdanos a mejorar esta aplicación. Todos los datos de uso se recopilan de forma anónima y se utilizan únicamente para mejorar la experiencia del usuario. + Configuraciones personales + Ayudas de accesibilidad + Mejoras del producto + Receta añadida + Receta ya importada + Se produjo un error durante la importación + PODER + Borrar + Receta escaneada + PIN olvidado + + %s Receta + %s Recetas + + He leído y acepto la política de privacidad y condiciones de uso. + Política de privacidad + Condiciones de uso + Lo haremos: + Mejorar la usabilidad. + Detectar errores y fallos. + Por supuesto, recopile todos los datos de forma anónima. + Puede modificar esta decisión en la configuración del sistema en cualquier momento. + Continuar + Aceptar + La aplicación utiliza el método más seguro que haya configurado en su dispositivo. + Ahorrar + Elegir + Medicamento + Nombre comercial + + No + Dosificación + Fecha de emisión + Esta receta se canjeará para usted como parte de un tratamiento. + No especificado + Pago adicional + Medicamento + Instrucciones de uso + Elegible según BVG + Preparación alternativa + Nombre de la receta + Embalaje + Instrucciones de fabricación + Descripción + dado por + emitido el: + Sustancia activa + Prescrito + Recibir + ¿Qué es una cesión directa? + Con la derivación directa, una receta de un consultorio o de un hospital se completa directamente en una farmacia. Los asegurados no tienen que realizar ninguna acción y no pueden intervenir en el proceso de canje.\n Las derivaciones directas se enumeran en la aplicación de recetas electrónicas para que su tratamiento sea más transparente para usted. + Tarifa de servicio de emergencia + Si se canjea una receta entre las 20.00 y las 6.00 horas o los domingos y festivos, se podrá cobrar un coste adicional de 2,50 euros. + Medicamentos sujetos a copago + Exento de pago adicional + Las personas aseguradas por ley suelen pagar un máximo de 10 euros por los medicamentos recetados. Si se solicita un medicamento de un fabricante determinado que no esté cubierto por un contrato de descuento con la compañía de seguros médicos (\"medicamento deseado\"), pueden aplicarse tarifas más altas.\n Los niños y jóvenes menores de 18 años están exentos de pagos adicionales.\n Si las recetas se canjean después de 28 días desde su emisión, los costos deben ser asumidos íntegramente por el paciente.\n Si los gastos de medicamentos son elevados a lo largo del año, se puede solicitar una exención del copago a la compañía de seguros médicos + Usted está exento de pagar un copago por este medicamento. Su compañía de seguro médico cubrirá el costo del medicamento. + ¿Por cuánto tiempo es válida esta receta? + Durante este periodo podrás canjear tu receta en cualquier farmacia con un pago adicional máximo de 10€. + Receta escaneada + Las recetas importadas desde una copia impresa no pueden mostrar información personal o médica por razones de seguridad.\n Inicie sesión en esta aplicación con su tarjeta sanitaria o aplicación de seguro para ver toda la información contenida en la receta. + Receta incorrecta + Esta receta fue emitida incorrectamente. + Tarifa de servicio de emergencia + Posología según instrucciones escritas + Teléfono + Sitio web + Correo electrónico + Comparte tu ubicación para encontrar farmacias cerca de ti. + DE ACUERDO + Introduzca el PIN actual + PIN incorrecto ingresado + El PIN actual de su tarjeta sanitaria + Tarjeta bloqueada + Desbloquea tu tarjeta en Configuración > Desbloquear tarjeta. + Por razones de seguridad, ingrese su PIN actual. + Olvidé mi PIN + Receta defectuosa + Medicamento + Parece que algo salió mal al crear tu receta. ¿Quieres informar un error? + Informe + No ha iniciado sesión + Registrado en + Tarjeta de seguro + Biometría + No ha iniciado sesión + Aviso de advertencia + Farmacia añadida a favoritos + Farmacia eliminada de favoritos + Mis farmacias + La operación de escritura no tuvo éxito + No se pudo guardar el PIN + Informe + Asignar PIN + Regla de acceso violada + No tienes permiso para acceder al directorio de mapas. + Asigna tu propio PIN + La tarjeta está protegida con un PIN de su compañía de seguros médicos (PIN de transporte). Introduzca su propio PIN. + Contraseña no encontrada + No hay ninguna contraseña almacenada en su tarjeta. + Se ha cerrado la sesión + Inicie sesión nuevamente para actualizar sus recetas. + Número de ingrediente activo + Potencia y Unidad + Canjeado hace %s minutos + Canjeado el %s + Recién redimido + Canjeado a las %s en punto + Esta receta fue canjeada para usted como parte de un tratamiento. + Tarifa de servicio de emergencia + Esta receta no se puede surtir en una farmacia por la noche sin el pago adicional de una tarifa por servicio de emergencia. + Busca aquí + Ajustes + Compartir ubicación en Configuración. + Cerca de mi + Mantenga presionado para editar el nombre. + Introduzca el nuevo nombre para el perfil. + Para recibir recetas de forma digital desde su consultorio, debe iniciar sesión. + ¿Recibir recetas digitalmente? + Baje la pantalla para actualizar. + Sin prescripciones + Regístrese para recibir recetas automáticamente. + Registro + Archivo de recetas + Quizás más tarde + Registro + Editar foto de perfil + Archivo de recetas + Introducir nombre + Ahorrar + Mi pedido + Destinatario: en + Recetas + Farmacia + Enviar + Cambiar + Recoger en la farmacia + Entrega por mensajería + Entrega por correo + %s Recetas + No es posible canjearlo + No se pudieron canjear una o más recetas. + No se ha seleccionado ninguna receta + Para canjear recetas, se debe seleccionar al menos una receta. + Añadir datos de contacto + Cambiar + Sin receta + Actualmente no tienes recetas canjeables + Levantar + Mensajero + Pedido por correo + Elija recetas + Toque aquí para escanear recetas + Mantenga pulsado para editar nombres + Añade más perfiles, por ejemplo, para tus hijos o padres. + Haga clic en la pantalla para omitir la información sobre herramientas que aparece. + ¿Cómo canjear? + ¿Cómo le gustaría recibir su medicamento? + Mostrar código + Hazte un escaneo en la farmacia + Enviar a farmacia + Reservar o recibirlo + Hecho + Código de colección + Códigos individuales + + Tienes %s receta. + Tienes %s recetas. + + Hacer una selección + Todas las recetas + ¿Qué recetas? + Próximo + Próximo + Saber más + Nota + Esta aplicación utiliza software de Google para reconocer códigos. + Saber más + Acerca del escáner de códigos de prescripción + ¿Qué datos contiene el código de receta? + El código de receta contiene únicamente un identificador de la receta. Esto permite encontrar la receta en el servicio de prescripción de la red de salud digital. El código de receta no contiene ningún dato sobre usted o su medicamento. + ¿Entonces nadie puede hacer nada sólo con el código de receta? + Correcto. Los datos de la receta se deben descargar del servicio de recetas. Para ello es necesario iniciar sesión de forma segura. + ¿Quién puede registrarse en el servicio de prescripción médica? + El registro para el servicio de prescripción médica en la red de salud digital es posible para asegurados, farmacias, consultorios y hospitales. + ¿Por qué la aplicación de receta electrónica utiliza funciones de Google? + Google ofrece funciones que se pueden integrar fácilmente en las aplicaciones y que Google desarrolla y actualiza constantemente. Esto garantiza que las funciones funcionen en muchos dispositivos finales diferentes y que se puedan utilizar de forma segura. La aplicación utiliza una función para mejorar la cámara y la función de escaneo para dispositivos Android (Google ML Kit). + ¿Cómo funciona la mejora del escaneo con Google ML Kit? + Google ML Kit ayuda a optimizar la imagen capturada por una cámara para que los códigos de prescripción puedan leerse incluso en condiciones de poca iluminación o con modelos de cámaras más antiguos. + ¿Se compartirán con Google datos sobre la receta o mi medicamento? + No. El código de receta leído se guarda directamente en la aplicación. No se transmite a Google. Los datos de la receta no se almacenan en el código, sino solo en la red de salud digital. Desde allí se envían a la aplicación. Google no tiene acceso a la red de salud digital. + ¿Qué datos procesa Google cuando utiliza ML Kit? + Google solo tiene acceso a información técnica sobre el dispositivo utilizado y el uso general de la función adicional (por ejemplo, tasa de errores, configuración de la cámara) para registrarlos estadísticamente y, de esta forma, mejorar la función adicional. Cuando accede, Google registra temporalmente la dirección IP de su dispositivo. Google no registra información sobre usted ni sobre el contenido de la receta. + ¿El uso de Google ML Kit es voluntario? + Sí. Sin embargo, el ML Kit está integrado en el escáner de códigos de recetas en la versión Android de la aplicación de recetas electrónicas. Si utiliza el escáner de códigos de recetas en un dispositivo Android, también se utilizará siempre la función ML Kit. Sin embargo, puede prescindir del escáner de códigos de recetas. Sus recetas también se pueden cargar en la aplicación si se registra en la red de salud digital con la tarjeta sanitaria electrónica o a través de la aplicación de su seguro médico. + ¿Puedo ver quién ha visto mis recetas? + Sí. Todos los accesos a tus datos quedan totalmente registrados en la red de salud digital. En la aplicación de receta electrónica puedes ver quién ha accedido a tus datos. + ¿Dónde puedo contactar si tengo preguntas sobre la aplicación o la receta electrónica? + Puede encontrar información detallada en la declaración de protección de datos. + Número de paquetes prescritos + Sin prescripciones + Para esto se necesitan recetas canjeables. + Seleccione compañía de seguros + Búsqueda de seguros + Cancelar + ¿A qué te gustaría postular? + Para esta aplicación necesitas una tarjeta y el PIN correspondiente. + ¿Cómo le gustaría contactar a su compañía de seguros? + Su compañía de seguros le ofrece las siguientes opciones de contacto + Su compañía de seguros le ofrece la siguiente opción de contacto + Cerca + PIN ingresado incorrectamente. + Número de acceso ingresado incorrectamente + PUK ingresado incorrectamente + Recibos de gastos + Ver recibos de gastos + Recibos de gastos + Para recibir recibos de costos, debe estar conectado al servidor. + Conectar + Recibos sin costo + Desactivar + Cancelar + Desactivar función + Esto eliminará todos los recibos de gastos de este dispositivo y del servidor. + Recibir recibos de gastos + Sus recibos de gastos también se guardan en el servidor de recetas. + Permitir + Total: %s %s + Seleccionar + Dividir + Borrar + Borrar + Entregar + %s € + Precio total + Consejo: Envíe los recibos de gastos a través de la aplicación de seguros + Envíe recibos de gastos fácilmente a través de la aplicación de su compañía de seguros. En el siguiente paso, seleccione esta aplicación y presione compartir. + Práctica + Farmacia + Fecha + Mostrar más + Identificación de medicamentos + Emitido para + KVNR: %s + Nacido el: %s + DE ACUERDO + ¿Cómo presentar los documentos de respaldo? + Transfiera directamente a la aplicación de su oficina de seguros/beneficios. Para ello, seleccione la aplicación en la página siguiente. + o + Guarde el archivo y luego impórtelo al portal de seguros/beneficios. + Artículo: %s + Conteo: %s + IVA: %s %% + Precio bruto en EUR: %s + Tarifas adicionales + Tarifa de servicio de emergencia + Tarifa BTM + Tarifa de prescripción T + Costos de adquisición + Mensajero + Total en EUR: %s + Deber + ¿Seguro, borrar? + El archivo se eliminará de su dispositivo y del servidor. + Borrar + Enviado + Código postal + Lugar + Será redimido por usted + Fue redimido por ti + Debe iniciar sesión para utilizar este servicio. + Tarjeta de seguro + Se requiere PIN asociado + Solo se puede canjear mañana como autopago + Solo quedan %s días para canjear como autopagador + Aún así, canjeable como autopago por %s días + Solo quedan %s días para canjear + Aún canjeable por %s días + Solo canjeable mañana + Se aplican cargos + Acepta seguro + Las recetas se han transferido correctamente. + No se puede procesar la receta. Inténtelo de nuevo. Es posible que deba elegir otra farmacia. + No se puede procesar la receta. La farmacia informa de un error desconocido. En caso necesario, pruebe con otra farmacia. + La farmacia rechazó la receta. Es posible que la receta no sea válida o que su dirección de entrega o información de contacto no sean válidas. + Inténtalo de nuevo y, si es posible, elige otra farmacia. Si el error persiste, ponte en contacto con el servicio de asistencia. + La receta se transfirió correctamente. Sin embargo, la farmacia informa de un error de procesamiento. Póngase en contacto con la farmacia. + La receta fue rechazada por la farmacia. La receta ya fue canjeada. + La farmacia rechazó la receta. La receta ha sido eliminada. + No se pudo transferir la receta. Verifique su conexión a Internet y vuelva a intentarlo. + No se pudieron transferir una o más recetas. + Error al enviar + ¡Enviado exitosamente! + Error en la farmacia + Error en la farmacia + Contactar con la farmacia + Receta ya canjeada + Receta eliminada + Sin Internet + Para recibir registros de acceso, debe estar conectado al servidor. + Durante este período, puede seguir comprando la receta en una farmacia, pero deberá pagar el precio total de compra del medicamento. Otra opción es solicitarle a su médico que le vuelva a emitir la receta. + Hecho + Solicitar corrección + En la farmacia + En la aplicación + Escanee este código en su farmacia. + Solicitud de corrección de facturación + Medicamento + Por favor, introduzca al menos 1 carácter. + O bien: Pruebe la aplicación en modo de demostración + Modo de demostración + Modo de demostración + Utilice el modo de demostración + Modo de demostración habilitado + Termina aquí + Activar el modo demo + Ahora no + Buscar + Recibir recibos de gastos digitalmente + Una vez activados los recibos de gastos los encontrarás aquí después de canjear tu receta. + Permitir + Recibo de gastos digital + Una vez que la farmacia haya depositado el recibo de coste, éste aparecerá aquí. + Recibo de gastos + Recibir recibos de gastos digitalmente + Nota: Ya no recibirá sus recibos de costos impresos en la farmacia. + Permitir + Quizás más tarde + Acuerdo + Con su consentimiento, se abstendrá de imprimirlos en la farmacia y en el futuro presentará sus recibos de gastos de forma digital. + Cancelar + Acordado + Salir del modo de demostración + Para obtener una visión general de los recibos de costos + A los recibos de gastos + Ahora recibirás los recibos de gastos de forma digital + Ya recibirás los recibos de gastos de forma digital + DE ACUERDO + Intentar otra vez + Función activada + Ya puedes recibir recibos de gastos de forma digital. + Sin Internet + No hay conexión a Internet. + Solicitud incorrecta + Hubo un problema con la solicitud. Estamos trabajando para solucionarlo. + El servidor no responde + Por favor, inténtelo de nuevo en unos minutos. + No se pudo conectar + Actualmente no se puede recuperar información. Inténtelo nuevamente más tarde. + Rechazado + El servidor rechazó tu solicitud. Vuelve a intentarlo más tarde. + Actualizar aplicación + Para utilizar esta función, actualice su aplicación. + error de inicio de sesion + El servidor tuvo un problema al iniciar sesión. Inicie sesión nuevamente. + Registro + Copiar URL + Conectarse a la aplicación de seguro médico externo. + No se puede encontrar la aplicación de seguro médico + No se pudo acceder a la aplicación del seguro médico. La autenticación no se realizó correctamente. + No es posible para este seguro de salud + Por favor, introduzca un número de teléfono para poder contactarle. + Por favor, introduzca un nombre y apellido para fines de contacto. + Por favor, introduzca una calle y número de casa para fines de contacto. + Proporcione su código postal para contactarnos. + Por favor indique su lugar de residencia para contactarnos. + El número de teléfono ingresado no es válido. (demasiado corto, demasiado largo, caracteres no válidos) + Por favor, introduzca una dirección de correo electrónico para contactarnos + La dirección de correo electrónico ingresada no es válida. + El texto ingresado no es válido (es demasiado largo, caracteres no válidos) + El texto ingresado no es válido (es demasiado largo, caracteres no válidos) + El texto ingresado no es válido (es demasiado largo, caracteres no válidos) + El texto ingresado no es válido (demasiado largo, demasiado corto, caracteres no válidos) + El texto ingresado no es válido (es demasiado largo, caracteres no válidos) + El texto ingresado no es válido (es demasiado largo, caracteres no válidos) + Datos incorrectos: se requiere corrección + Se requiere aplicación adicional + Recomendado + Identificación sanitaria digital + ¿Ajustar la configuración? + Habilite la apertura de enlaces en su configuración para continuar. + Abrir configuración + Versión de la aplicación obsoleta + Su versión de la aplicación está desactualizada y ya no es compatible. Para continuar utilizando la receta electrónica, actualice la aplicación. + Actualizar + Su número de acceso tiene %s dígitos. + Su PIN puede tener entre %s y %s dígitos. + Levantar + Mensajero + Envío + Se requiere un teléfono inteligente con NFC + No pudimos procesar la solicitud de eliminación de la receta. Estamos trabajando en una solución ( %s ). + Algo salió mal al eliminar mi receta, recibí el código de error %s . Por favor, avíseme cuando sea posible volver a eliminarlo. + Próximo + Solicitud incorrecta + Informe + Eliminar localmente + Sin Internet + No se pudo eliminar la receta. Verifique su conexión a Internet y vuelva a intentarlo. + Intentar otra vez + DE ACUERDO + Reiniciar sesión + Debe iniciar sesión para eliminar la receta (401). + Acceso + Cancelar + Eliminación imposible + No se le permite eliminar la receta en este momento porque está en la farmacia para ser surtida o es una asignación directa pendiente (403). + Demasiadas solicitudes + Has intentado eliminar la receta demasiadas veces. Vuelve a intentarlo más tarde (429). + Eliminar receta + Ups... + \"Se produjo un error inesperado en el servidor. Inténtelo nuevamente más tarde (500)\". + No es posible utilizar la seguridad de la aplicación. Configure previamente la seguridad biométrica (por ejemplo, huella dactilar, FaceID) en su dispositivo. + La copia de seguridad biométrica no es posible porque su dispositivo no la admite. + ¡Atención! Tu teléfono no está bien protegido. + Sus datos de inicio de sesión se guardan en su teléfono. Para proteger el acceso, se utiliza su método de inicio de sesión preferido. Este método de inicio de sesión, por ejemplo, el patrón de deslizamiento, no es muy seguro. Usted utiliza la función \"Guardar datos de inicio de sesión\" bajo su propio riesgo. + Si sigue utilizando la función \"Guardar datos de inicio de sesión\", en el futuro podrá ver, acceder, canjear o eliminar recetas con la aplicación de receta electrónica sin necesidad de una tarjeta sanitaria e introduciendo el PIN. + Ubicación no encontrada + Enviar a farmacia + Mostrar código + Idioma + Idioma + Alemán + árabe + búlgaro + checo + danés + Inglés + Francés + hebreo + italiano + Holandés + Polaco + rumano + ruso + turco + ucranio + Por defecto + Los recibos de gastos digitales han sido desactivados y eliminados. + "Foro de recetas electrónicas" + No es posible sustituir el producto + Posible medicación sustitutiva + Medicamento de reemplazo (Aut idem) + Los farmacéuticos están obligados a dispensar con carácter prioritario los medicamentos para los que la compañía de seguros médicos del paciente haya celebrado un acuerdo de descuento con los fabricantes de medicamentos. Esto no se aplica únicamente si el médico excluye la mención \"Aut idem\" en la receta, lo que no sucede en el caso de su receta. + Su médico ha determinado que debe recibir el medicamento recetado. La farmacia no debe realizar ningún cambio en base a un acuerdo de descuento (“Aut idem”). + Permitir capturas de pantalla + Si permites las capturas de pantalla, la última página que hayas abierto permanecerá visible en segundo plano cuando cambies de aplicación. Como resultado, tus datos personales podrían quedar visibles. Por lo tanto, te recomendamos que no permitas las capturas de pantalla. + Permitir + Cancelar + Cambia tu teclado a stickers o usa emojis para tu foto de perfil. + Elige una foto de perfil + ¿Cómo te gustaría continuar? + Seleccionar foto + Cámara + Emoji + Cancelar + Abrir configuración + Usar + Captura + Editar foto de perfil + Aceptado hace %s minutos + Aceptado el %s + Recién aceptado + Aceptado a las %s en punto + ID de salud + Seleccione compañía de seguros + Si el registro con el ID de salud no funciona como se espera, siga los consejos de nuestra ayuda. + Ayuda + Ayuda + Consejos para registrarse en la aplicación de seguros + Su compañía de seguros es responsable del documento de identidad sanitario. Póngase en contacto con ellos si tiene alguna pregunta sobre el registro. A continuación, se ofrecen algunos consejos probados y comprobados: + Tenga en cuenta que, según su seguro, se requiere una aplicación independiente. Pregunte a su compañía de seguros cuál es. + Abra la aplicación de seguros e inicie sesión allí una vez antes de comenzar a iniciar sesión en la aplicación de receta electrónica. + Cambiar entre iniciar sesión con su identificación sanitaria y su tarjeta sanitaria puede causar problemas. Cierre la sesión de su perfil antes de cambiar la opción de inicio de sesión. + Si tu seguro no está en el listado, también puedes iniciar sesión con tu tarjeta sanitaria y el PIN correspondiente. + Si la aplicación de la aseguradora de salud no lo redirecciona nuevamente a la aplicación de receta electrónica, asegúrese de informar este error a su aseguradora. + Si no se puede acceder a la aplicación de seguros, puede ser útil ir a la configuración del navegador de su teléfono inteligente y permitir que se abran los enlaces. + Si su teléfono inteligente ejecuta Android 14, puede ser útil permitir la apertura de enlaces en la configuración. + Abrir configuración + Desafortunadamente, eso no funcionó. + Por favor, inténtelo de nuevo más tarde. + Mensajes de error al cargar. + Por favor seleccione un emoji o texto + Ahorrar + Por favor, inicia sesión para continuar + Próximo + Llamar + Escribir correo \n + A la farmacia + Farmacia + Hoy + Mañana + Ruta\naquí + Eliminar receta y recibo de gastos + Si elimina la receta, también se eliminará el recibo de gasto asociado. + Los mensajes de todos los perfiles ahora se muestran juntos + Nuevas funciones + Nuevas funciones + Trabajamos constantemente en nuevas funciones. Cuéntanos qué opinas y ayúdanos a mejorar la aplicación. + Pedidos para todos + En Pedidos, ahora puedes ver los pedidos de todos los perfiles juntos y visualizarlos mucho más fácilmente. + + No hay reembolso de gastos + Por regla general, el seguro médico no cubre los costes de esta receta, por lo que, como paciente, usted es responsable de pagar el importe íntegro. Debe comprobar individualmente si los costes se reembolsarán como parte de un seguro complementario o de una prestación legal. + Su seguro no cubrirá el costo de esta receta. + + "Su seguro no cubre la receta %s " + \"Su seguro no cubre las recetas %s \". + + Causado + Fecha + Accidente + Accidente de trabajo + Enfermedad profesional + Mensajes + No hay mensajes + Aún no tienes ningún mensaje. + 🎉Tu pedido está listo para ser recogido. Muestra este código de recogida para identificarte. + Lamentablemente, el mensaje de su farmacia estaba vacío. Por favor, póngase en contacto con su farmacia. + La farmacia le ha proporcionado un enlace. + Mensajes + Orden + Mensajes + Sin conexión a Internet + Para mostrar los dispositivos conectados, debe estar conectado mediante biometría. + La receta fue eliminada el %s + Eliminado + Sin prescripciones + No tienes recetas en el archivo. + Sus recibos de gastos han sido eliminados + Recibo de gastos + Es necesario registrarse + Inicie sesión para ver los dispositivos conectados. + Acceso + Acceso + Conectando a %s Seguros + Tarjeta sanitaria en tu smartphone + No ha iniciado sesión + Iniciado sesión + refrescar + Recibirá registros de acceso cuando inicie sesión en el servicio de recetas. + Es necesario registrarse + Inicie sesión con su tarjeta sanitaria y guarde sus datos de acceso con biometría para ver los dispositivos conectados. + %1$s x Mañana + %1$s x mediodía + %1$s x noche + %1$s x Noche + A menos que su médico le indique lo contrario, las instrucciones de uso pueden entenderse de la siguiente manera: + Su médico le ha dado esta información sobre cómo tomar su medicamento. + Su receta no contiene ninguna información sobre las instrucciones de dosificación. + Su médico ha observado que le han dado instrucciones sobre cómo tomar su medicamento que no están en la receta. Por ejemplo, esto puede estar en su plan de medicación. + No especificado + DJ + Instrucciones de dosificación + " %s - %s" + recordatorio de medicación + recordatorios de medicamentos + medicamentos orales + A los recordatorios de medicación + mañana + hora de comer + tarde + Por la tarde + Medicamento + Información de dosificación + 1 + %s / %s + 1 dosis + cambiar dosis + Cancelar + recordatorio de medicación + plan de medicación + Tome la dosis prescrita por su médico. + A las %s en punto toma %s x + Seleccionar fechas + Próximo + Cancelar + En + Apagado + Seguimiento + Ilimitado + Individualmente + Primer día + Día último + Añadir tiempo + Sin recordatorios de pastillas + Puede configurar recordatorios para sus recetas + encendido + apagado + Recuérdame + Desactivar la optimización de la batería para esta aplicación. + Si no activa esta opción, es posible que los recordatorios de medicamentos se muestren de forma poco confiable. + Cancelar + Permitir + Instrucciones de dosificación + No especificado + información + Recuerde también en modo de optimización de batería + cambiar dosis + Cantidad + forma + finalizado + ilimitado + a %s + Repetir %s + Siempre que sea posible, utilizamos la información almacenada en la receta para el cálculo. + Cancelar + Ahorrar + Hasta el final del paquete + Tiempo + dosificación + Mostrar medicamentos + ¿Ya has tomado tu medicación? + recordatorios de medicamentos + No hay recordatorios activos + No tienes recordatorios para tomar hoy + Seleccione fecha de inicio + Seleccionar fecha de finalización + Bienvenido + en tu aplicación de recetas electrónicas + Intentar otra vez + Introduce la contraseña + Asegúrese de que cualquier persona con la que pueda compartir este dispositivo y que conozca su información de inicio de sesión también tenga acceso a sus recetas. + Introduce la contraseña + Ingrese la contraseña para desbloquear la aplicación. + contraseña + Contraseña + Ingrese una contraseña para proteger la aplicación. + Mostrar contraseña + Repita la contraseña + ¿Olvidaste tu contraseña? Elimine la aplicación y luego vuelva a instalarla. Puedes descubrir por qué en nuestro %s. + Introduce la contraseña + La contraseña debe tener al menos ocho caracteres. + La seguridad de la contraseña no es suficiente + La seguridad de la contraseña es suficiente + No es posible realizar una copia de seguridad del dispositivo + Primero configure una copia de seguridad del dispositivo. + Cancelar + Ajustes + La seguridad de la contraseña es muy buena. + Seguridad de la aplicación + Copia de seguridad del dispositivo + Elija al menos un método para hacer una copia de seguridad de la aplicación. + contraseña + Cambiar la contraseña + Para eliminar, se debe establecer una conexión con Prescription Server + Los productos están listos. + Los productos han estado listos desde %s + Los productos han estado listos hace un momento. + Los productos han estado listos durante %s minutos + Los productos han estado listos desde %s + Deshabilitado porque no se ingresa ninguna contraseña. + Deshabilitado porque la contraseña es demasiado débil. + Deshabilitado porque las contraseñas no coinciden. + Explorar + registro de donación de órganos + ¿Abrir registro de donación de órganos? + Serás redirigido al registro de donantes de órganos. Para poder ver y cambiar sus datos de donación de órganos, debe iniciar sesión allí. + Abierto + Cancelar + Función no activa en modo demo + diff --git a/app/features/src/main/res/values-fr/strings.xml b/app/features/src/main/res/values-fr/strings.xml index e727a39b..41c80f10 100644 --- a/app/features/src/main/res/values-fr/strings.xml +++ b/app/features/src/main/res/values-fr/strings.xml @@ -26,7 +26,7 @@ N\'annulez pas Allons-y De quoi as-tu besoin: - Entrez le numéro d\'accès de la carte + Entrez le numéro d\'accès entrez le code PIN Essayer à nouveau Impossible de se connecter au serveur. @@ -55,26 +55,23 @@ imprimer éditeur Gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin - Directeur général : Dr. Florian Hartge\n Tribunal d\'enregistrement : tribunal de grande instance de Berlin-Charlottenbourg\n Numéro de registre du commerce : HRB 96351\n Numéro d\'identification TVA : DE241843684 + Gestion : Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nTribunal d\'enregistrement : tribunal d\'instance de Berlin-Charlottenbourg\nNuméro de registre du commerce : HRB 96351\nNuméro d\'identification TVA : DE241843684 Responsable du contenu - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Contact Avis Nous nous efforçons d’utiliser un langage équitable entre les sexes. Si vous remarquez des erreurs, nous serions heureux de vous entendre par e-mail. La plateforme moderne allemande pour la médecine numérique Écrire un email Ouvrir le site Web - Accueillir - Commencer l\'inscription Ouvrir Registre Annuler - Sécurité Légal imprimer protection des données Conditions d\'utilisation - Détails + Détails de la recette Marquer comme utilisé Marquer comme non utilisé Forme posologique @@ -96,8 +93,6 @@ Numéro d\'usine Numéro de téléphone Adresse e-mail - Accident du travail - Jour de l\'accident Numéro d\'entreprise ou d\'employeur accidenté Souhaitez-vous supprimer définitivement cette recette ? Supprimer @@ -120,7 +115,6 @@ Scanner ouvert pour les ordonnances Paramètres Supprimer les captures d\'écran - Empêche l\'affichage d\'une image d\'aperçu lors du changement d\'application Autorisez-vous E-Prescription à analyser votre comportement d’utilisation de manière anonyme ? Informations techniques Sécurité de vos données de prescription @@ -142,23 +136,15 @@ L\'analyse anonyme reste désactivée %s Merci pour votre soutien ! Registre - Veuillez vous identifier pour télécharger des recettes. - Remarque pour les pharmacies : Nous obtenons les coordonnées et les informations sur les pharmacies auprès de mein-apothekenportal.de de l\'Association allemande des pharmaciens. Avez-vous découvert une erreur ou souhaitez-vous corriger des données ? + Veuillez vous identifier. + Remarque pour les pharmacies : Nous obtenons les coordonnées et les informations sur les pharmacies auprès de mein-apothekenportal.de. Vous avez découvert une erreur ou souhaitez corriger des données ? Apprendre encore plus Pharmacies Malheureusement, cela n\'a pas fonctionné \uD83D\uDE15 Veuillez réessayer. - Entrer le mot de passe - Plus loin Accessibilité - Zoom - Vous permet d\'agrandir l\'application en pinçant pour zoomer. - mot de passe - Sécurisez vos données avec un mot de passe de votre choix. - mot de passe + Activer le zoom Sauvegarder - Montrer le mot de passe - Répéter le mot de passe Recommandations : %s Écrire un email Lorsque vous envoyez votre message, les informations suivantes sur le matériel et le système d\'exploitation utilisé sont transmises : @@ -169,7 +155,7 @@ Expédition filtre Filtre - Aucun emplacement disponible + Partager l\'emplacement Compris Correspondances de mot de passe répétées Erreur 20 10 76631 @@ -179,8 +165,6 @@ %s tentatives de connexion infructueuses ont été détectées. %s tentatives de connexion infructueuses ont été détectées. - Choisissez la meilleure sauvegarde de périphérique - Il peut s\'agir d\'une empreinte digitale, d\'un motif de balayage ou de quelque chose de similaire Jetons Jetons d\'accès Jetons SSO @@ -188,7 +172,7 @@ aucun jeton SSO disponible copié dans le presse-papiers Cliquez pour copier le jeton dans le presse-papiers - Uniquement valable aujourd\'hui + Échangeable uniquement aujourd\'hui Permettre pas de connexion au serveur S\'il vous plait, réessayez dans quelques minutes @@ -254,7 +238,7 @@ %s nouvelles recettes Rachetable - En rédemption + Est en cours de traitement Racheté Inconnu Afficher les journaux d\'accès @@ -266,7 +250,7 @@ La recette est actuellement en cours et ne peut pas être supprimée Accepter Apparemment, ça n\'a pas fonctionné - Nous sommes conscients que le lien avec la carte santé comporte ses pièges. À l’avenir, l’inscription devrait également être possible via une application d’assurance maladie déjà authentifiée. \n\n Nous veillons également à ce que les ordonnances puissent être échangées numériquement sans inscription. \n\n Avez-vous remarqué quelque chose au cours de ce processus que vous aimeriez partager avec nous ? N\'hésitez pas à nous écrire, nous serions également heureux de recevoir des commentaires très critiques. + Nous sommes conscients que le lien avec la carte santé comporte ses pièges. À l’avenir, l’inscription devrait également être possible via une application d’assurance maladie déjà authentifiée.\n\nNous veillons également à ce que les ordonnances puissent être utilisées numériquement sans inscription.\n\nAvez-vous remarqué quelque chose au cours de ce processus que vous aimeriez partager avec nous ? N\'hésitez pas à nous écrire, nous serions également heureux de recevoir des commentaires très critiques. Conseils de connexion Améliorer la force de la connexion Si nécessaire, retirez le capot de protection. @@ -289,7 +273,7 @@ Scanné sur %s Marqué comme utilisé le %s Comment veux-tu continuer ? - Commande + Envoyer à la pharmacie Bientôt disponible Réservez maintenant pour la collecte ou faites-vous livrer par coursier ou par expédition Sauvegarder pour une commande ultérieure @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Protection et utilisation des données Plus loin - Vous avez reçu le code PIN de votre carte maladie de votre caisse d\'assurance maladie via une procédure sécurisée telle que Postident. + Vous deviez commander activement le code PIN de votre carte de santé auprès de votre caisse d\'assurance maladie, puis le recevoir via un processus sécurisé tel que Postident. Aucun code PIN reçu code PIN Vérifiez la connexion Internet et les paramètres d\'heure et de date de votre appareil. - Pour vous connecter, appuyez sur « Déverrouiller ». Enfermé dehors? Veuillez vérifier vos informations d\'identification biométriques sur cet appareil. - Mot de passe oublié? Veuillez supprimer l\'application, puis la réinstaller. Vous pouvez découvrir pourquoi dans notre %s . zone d\'aide Taille du conditionnement et unité ingrédient actif @@ -338,10 +320,6 @@ Défait Avis Aidez-nous à améliorer cette application - Entrer le mot de passe - Le mot de passe doit comporter au moins huit caractères - La force du mot de passe n\'est pas suffisante - Force du mot de passe suffisante Le mot de passe est visible Le mot de passe n\'est pas visible biométrie @@ -446,9 +424,6 @@ Envoyé à l\'instant Envoyé à %s heure N\'est plus valide - Inscrivez-vous avec l\'application - Choisissez une assurance - Vous n\'avez pas trouvé ce que vous cherchiez ? Cette liste est constamment élargie. L\'inscription avec une carte de santé est déjà prise en charge par chaque caisse d\'assurance maladie. Retour d’expérience de l’application e-prescription Nous attendons avec impatience vos commentaires. Veuillez utiliser l\'espace suivant et être aussi précis que possible : PUK @@ -468,7 +443,7 @@ Avis Accepter Sécurité de vos données de prescription - \"Cette application utilise le capteur biométrique le plus sécurisé fourni par votre appareil pour sécuriser vos informations d\'identification dans une zone protégée du stockage de l\'appareil.\" + \Cette application utilise le capteur biométrique le plus sécurisé fourni par votre appareil pour sécuriser vos informations d\'identification dans une zone protégée du stockage de l\'appareil.\ La sécurité biométrique de vos données d\'accès vous permet d\'ouvrir cette application à l\'avenir, de consulter, de récupérer, d\'utiliser ou de supprimer des ordonnances sans carte de santé et sans saisir votre code PIN. Veuillez vous assurer que les personnes avec lesquelles vous pouvez partager cet appareil et dont les caractéristiques biométriques peuvent être stockées sur cet appareil ont également accès à vos ordonnances. cela n\'a malheureusement pas fonctionné @@ -481,7 +456,7 @@ Nom de famille Assurance Numéro d\'assurance - Numéro d\'accès à la carte + Numéro d\'accès Registre Se déconnecter Sauvegarder @@ -497,8 +472,7 @@ connexion perdue Vous connecter au serveur de recettes maintenant ? Pas de jetons - Vous recevrez un jeton lorsque vous serez connecté au service de prescription.\n - Ordres + Vous recevrez un jeton lorsque vous serez connecté au service de prescription. Sélectionnez le code PIN souhaité Débloquer la carte Sélectionnez le code PIN @@ -515,26 +489,26 @@ Code de retrait reçu Le message ne peut pas s\'afficher Veuillez contacter votre pharmacie ( %s ). - Afficher le lien du panier + Lien pharmacie Afficher le code de retrait Afficher le message %s à %s heures - Recette envoyée à %s . Le processus de remboursement numérique est encore peu familier à de nombreuses pharmacies. Si vous n\'avez pas de réponse d\'ici demain, nous vous recommandons d\'appeler pour vous renseigner par mesure de précaution. + Recette envoyée à %s . Certaines pharmacies ne disposent pas encore d’option de réponse numérique. S\'il n\'y a pas de réponse d\'ici demain, veuillez appeler par mesure de précaution. Aperçu de la commande Nouveau Cours L\'ordre - Gratuit pour l\'appelant. Horaires de service : du lundi au vendredi de 8h00 à 20h00 sauf les jours fériés + Gratuit pour l\'appelant. Horaires de service : du lundi au vendredi de 8h00 à 20h00 sauf les jours fériés fédéraux Pharmacie Sélectionnez le code PIN souhaité Code PIN souhaité enregistré Actuellement ouvert et près de chez moi Filtrer par … - lancer la recherche + Chercher Affectation directe Pharmacies Numéro de téléphone (facultatif) - Rechercher par nom ou adresse + Chercher Aucune information pharmaceutique valide Aucune information actuelle n\'a été trouvée sur cette pharmacie. L\'entrée pour cette pharmacie sera supprimée. D\'ACCORD @@ -554,16 +528,15 @@ Améliorations du produit Analyse anonyme Aidez-nous à améliorer cette application. Toutes les données d\'utilisation sont collectées de manière anonyme et sont utilisées exclusivement pour améliorer l\'expérience utilisateur. - Sécurité des applications paramètres personnels Accessibilité Améliorations du produit Recette ajoutée Recette déjà disponible Une erreur s\'est produite lors de l\'importation + Numéro d\'accès Supprimer Recette numérisée - Préparation de remplacement possible Code PIN oublié %s Recette @@ -579,7 +552,7 @@ Vous pouvez modifier cette décision à tout moment dans les paramètres système. Continuer Accepter - Cette application utilise la méthode la plus sûre fournie par votre appareil. + L\'application utilise la méthode la plus sûre que vous avez configurée sur votre appareil. Sauvegarder Choisir médicament @@ -607,15 +580,13 @@ Qu\'est-ce qu\'une mission directe ? Avec référence directe, une ordonnance d\'un cabinet ou d\'un hôpital est exécutée directement en pharmacie. Les assurés n’ont aucune démarche à effectuer et ne peuvent intervenir dans le processus de rachat. \n\n Les références directes sont répertoriées dans l\'application de prescription électronique pour rendre votre traitement plus transparent pour vous. Frais de service d\'urgence - Il faut parfois se dépêcher. Certaines ordonnances peuvent être exécutées sans le paiement supplémentaire de frais de service d\'urgence, par exemple la nuit ou les jours fériés. + Si une ordonnance est exécutée entre 20 heures et 6 heures du matin ou les dimanches et jours fériés, un supplément de 2,50 euros pourra être facturé. Médicaments soumis au ticket modérateur Exonéré de paiement supplémentaire - Les personnes bénéficiant d\'une assurance maladie légale doivent payer un supplément pouvant aller jusqu\'à dix euros pour les médicaments sur ordonnance. \n\n Le montant du supplément dépend du prix de vos médicaments. Vous devez payer vous-même les médicaments qui coûtent moins de 5 €.\n Pour les médicaments plus chers, vous devez payer dix pour cent du prix, mais au moins 5 € et au maximum 10 €. \n\n Les enfants et les jeunes de moins de 18 ans sont généralement exonérés du paiement supplémentaire. \n\n Si vos frais annuels de médicaments dépassent votre plafond de charge financière, vous pouvez être exonéré du ticket modérateur. Parlez-en à votre caisse d’assurance maladie. + Les personnes bénéficiant d\'une assurance maladie obligatoire paient généralement au maximum 10 euros pour les médicaments sur ordonnance. Des frais plus élevés peuvent s’appliquer si la demande d’un médicament auprès d’un fabricant spécifique n’est pas couvert par un accord de réduction d’assurance maladie (« demande de médicament »). \n\n Les enfants et les jeunes de moins de 18 ans sont exonérés des paiements supplémentaires. \n\n Si les ordonnances sont rachetées plus de 28 jours après leur émission, les frais doivent être intégralement supportés. \n\n Si vous dépensez beaucoup de médicaments au cours de l\'année, vous pouvez demander une exemption du ticket modérateur auprès de votre mutuelle. Vous êtes exonéré du paiement d’une quote-part pour ce médicament. Votre compagnie d’assurance maladie prendra en charge le coût des médicaments. Quelle est la durée de validité de cette prescription ? Pendant cette période, vous pouvez faire racheter votre ordonnance dans n\'importe quelle pharmacie moyennant un supplément de 10 € maximum. - Préparation de remplacement possible - En raison des exigences légales de votre compagnie d\'assurance maladie, une alternative avec le même principe actif peut vous être proposée. \n\n Les médicaments peuvent avoir une apparence et un nom différents, avoir des prix et des fabricants différents, mais contenir toujours le même principe actif. Le principe actif lui-même et le dosage sont déterminants pour l’effet des médicaments sur l’organisme. Les patients reçoivent souvent à la pharmacie un médicament différent de celui prescrit par le médecin, à condition que le médicament soit comparable. Il peut y avoir des raisons thérapeutiques et économiques à ce changement. Recette numérisée Les ordonnances importées à partir d\'une copie papier ne peuvent pas afficher d\'informations personnelles ou médicales pour des raisons de sécurité. \n\n Connectez-vous à cette application avec votre carte de santé ou votre application d\'assurance pour afficher toutes les informations contenues dans l\'ordonnance. Recette incorrecte @@ -625,7 +596,7 @@ téléphone site web Mail - Tri par distance impossible. + Partagez votre position pour trouver des pharmacies près de chez vous. D\'ACCORD Entrez le code PIN actuel Mauvais code PIN saisi @@ -643,12 +614,10 @@ Carte de santé biométrie Pas connecté - Votre avis nous intéresse. Veuillez prendre cinq minutes pour répondre à notre enquête. Merci beaucoup d\'avance. Avis d\'avertissement Pharmacie ajoutée aux favoris Pharmacie supprimée des favoris Mes pharmacies - Force du mot de passe très bonne L\'opération d\'écriture n\'a pas réussi Le code PIN n\'a pas pu être enregistré Rapport @@ -667,7 +636,6 @@ Échangeé le %s Récupéré tout à l\'heure Échangeé à %s heures - Ordres Cette ordonnance a été exécutée dans le cadre d’un traitement pour vous. Frais de service d\'urgence Cette ordonnance ne peut être exécutée la nuit en pharmacie sans le paiement supplémentaire des frais de service d’urgence. @@ -681,7 +649,7 @@ Recevoir des ordonnances par voie numérique ? Déroulez l’écran pour actualiser. Aucune recette - Connectez-vous pour recevoir automatiquement des recettes ou ajoutez une nouvelle recette en utilisant le ⊕ dans le coin supérieur. + Inscrivez-vous pour recevoir automatiquement les recettes. Registre Archives de recettes Peut-être plus tard @@ -718,9 +686,9 @@ Cliquez sur l\'écran pour ignorer l\'info-bulle qui apparaît. Comment racheter ? Comment souhaiteriez-vous recevoir vos médicaments ? - Échangez directement - Échanger des médicaments sur place - Commande + Afficher le code + Faites-le scanner à la pharmacie + Envoyer à la pharmacie Réservez ou faites-vous livrer Prêt Code de collecte @@ -786,7 +754,7 @@ Cela supprimera tous les reçus de dépenses de cet appareil et du serveur. Recevoir des reçus de coûts Vos justificatifs de frais sont également enregistrés sur le serveur de recettes. - Reçu + Activer Total : %s %s Choisir Diviser @@ -835,17 +803,17 @@ Code PIN associé requis Ne peut être utilisé que demain en tant qu\'auto-paieur Il ne reste que %s jours à utiliser en tant que payeur autonome - \nToujours échangeable en tant qu\'auto-paiement pendant %s jours\n - Valable pendant %s jours seulement - \nValable pour %s jours restants\n - Uniquement valable demain + Toujours échangeable en tant qu\'auto-paiement pendant %s jours + Il ne reste que %s jours pour profiter + Toujours échangeable pendant %s jours + Échangeable uniquement demain Des frais s\'appliquent Prend une assurance La ou les recettes ont été transférées avec succès. La recette ne peut pas être traitée. Veuillez réessayer. Vous devrez peut-être choisir une autre pharmacie. La recette ne peut pas être traitée. La pharmacie signale une erreur inconnue. Si nécessaire, essayez une autre pharmacie. L\'ordonnance a été rejetée par la pharmacie. L’ordonnance peut être invalide ou votre adresse de livraison ou vos coordonnées peuvent être invalides. - Impossible d\'échanger, veuillez vérifier votre connexion Internet. + Essayez à nouveau et choisissez éventuellement une autre pharmacie. Si l\'erreur persiste, veuillez en informer le support. La recette a été transférée avec succès. La pharmacie signale cependant une erreur de traitement. Veuillez contacter la pharmacie. L\'ordonnance a été rejetée par la pharmacie. L\'ordonnance a déjà été utilisée. L\'ordonnance a été rejetée par la pharmacie. La recette a été supprimée. @@ -878,7 +846,6 @@ Activer le mode démo Pas maintenant Recherche - Smartphone compatible NFC requis Recevez des reçus de coûts par voie numérique Une fois les reçus de frais activés, vous les retrouverez ici après avoir récupéré votre ordonnance. Activer @@ -886,11 +853,11 @@ Dès que la pharmacie aura déposé le reçu des frais, celui-ci apparaîtra ici. Reçu des coûts Recevez des reçus de coûts par voie numérique - Remarque : Vous ne recevrez plus vos justificatifs de frais sous forme imprimée dans le\n Pharmacie.\n + Remarque : vous ne recevrez plus vos reçus de frais sous forme imprimée à la pharmacie. Activer Peut-être plus tard accord - Avec votre accord, vous vous abstiendrez de l\'imprimer en pharmacie\n et soumettez vos reçus de coûts par voie numérique à l\'avenir.\n + Avec votre accord, vous renoncerez à les imprimer à la pharmacie et vous transmettrez à l’avenir vos justificatifs de frais par voie numérique. Annuler Convenu Quitter le mode démo @@ -905,17 +872,17 @@ Pas d\'Internet Il n\'y a pas de connexion à Internet. Demande incorrecte - Il y a eu un problème avec la demande. Nous travaillons sur une solution.\n + Il y a eu un problème avec la demande. Nous travaillons sur une solution. le serveur ne répond pas S\'il vous plait, réessayez dans quelques minutes. Échec de connexion - Aucune information ne peut actuellement être récupérée. S\'il te plaît\n Réessayez plus tard.\n + Aucune information ne peut actuellement être récupérée. Veuillez réessayer plus tard. Rejeté - Le serveur a rejeté votre demande. Essayez-le\n encore plus tard.\n + Le serveur a rejeté votre demande. Veuillez réessayer plus tard. Mettre à jour l\'application - Pour utiliser cette fonctionnalité, veuillez mettre à jour votre application.\n + Pour utiliser cette fonctionnalité, veuillez mettre à jour votre application. La connexion a échoué - Le serveur a rencontré un problème de connexion. Contactez-nous\n encore.\n + Le serveur a rencontré un problème de connexion. Veuillez vous reconnecter. Registre Copier le lien Connectez-vous à l\'application externe d\'assurance maladie. @@ -940,13 +907,286 @@ Application supplémentaire requise Recommandé ID de santé numérique - Ajuster les paramètres ? - 404 pages ? + Ajuster les paramètres? Veuillez activer l\'ouverture des liens dans vos paramètres pour continuer. Ouvrir les paramètres Version de l\'application obsolète Votre version de l\'application est obsolète et n\'est plus prise en charge. Pour continuer à utiliser l\'ordonnance électronique, veuillez mettre à jour l\'application. Mettre à jour - Votre CAN comporte %s chiffres. + Votre numéro d\'accès comporte %s chiffres. Votre code PIN peut comporter %s à %s chiffres. + Ramasser + Courrier + Expédition + Smartphone compatible NFC requis + Nous n\'avons pas pu traiter la demande de suppression de la recette. Nous travaillons sur une solution ( %s ). + Quelque chose s\'est mal passé lors de la suppression de mon ordonnance, j\'ai reçu le code d\'erreur %s . Veuillez m\'avertir lorsque la suppression sera à nouveau possible. + Plus loin + Demande incorrecte + Rapport + Supprimer localement + Pas d\'Internet + La recette n\'a pas pu être supprimée. S\'il vous plaît, vérifiez votre connexion à internet et réessayez. + Essayer à nouveau + D\'ACCORD + Reconnectez-vous + Vous devez être connecté pour supprimer la recette (401). + Registre + Annuler + Suppression impossible + Vous n\'êtes pas autorisé à supprimer l\'ordonnance pour le moment car elle est à la pharmacie pour être exécutée ou il s\'agit d\'une affectation directe en attente (403). + Trop de demandes + Vous avez essayé de supprimer la recette trop souvent. Veuillez réessayer plus tard (429). + Supprimer la recette + Oops... + "Une erreur de serveur inattendue s\'est produite. Veuillez réessayer plus tard (500)." + La sécurité de l\'application n\'est pas possible. Au préalable, configurez la sécurité biométrique (par exemple empreinte digitale) sur votre appareil. + La sauvegarde biométrique n\'est pas possible pour cet appareil car elle n\'est pas prise en charge par votre appareil. + Danger! Votre téléphone n\'est pas bien protégé. + Vos informations de connexion sont enregistrées sur votre téléphone. Pour protéger l\'accès, votre méthode de connexion préférée est utilisée. Cette méthode de connexion, par exemple le modèle de balayage, n\'est pas très sécurisée. Vous utilisez la fonction « Enregistrer les informations de connexion » à vos propres risques. + Si vous utilisez toujours la fonction « Enregistrer les données de connexion », vous pourrez à l\'avenir consulter, accéder, utiliser ou supprimer des ordonnances avec l\'application e-prescription sans carte de santé et sans saisir le code PIN. + Emplacement introuvable + Envoyer à la pharmacie + Afficher le code + Langue + Langue + Allemand + Arabe + Bulgare + Tchèque + Danois + Anglais + Français + Hébreu + Italien + Néerlandais + Polonais + Roumain + Russe + Turc + Ukrainien + Défaut + Les reçus de frais numériques ont été désactivés et supprimés. + « Forum e-prescription » + Aucune préparation de substitution possible + Préparation de remplacement possible + Préparation de substitution (Aut idem) + Les pharmaciens sont tenus de délivrer en priorité les médicaments pour lesquels la caisse d\'assurance maladie du patient a conclu un accord de réduction avec les fabricants de médicaments. Ceci ne s’applique que si le médecin exclut « Aut idem » sur l’ordonnance, ce qui n’est pas le cas sur votre ordonnance. + Votre médecin a déterminé que vous devriez recevoir la préparation prescrite. La pharmacie ne doit pas procéder à un échange sur la base d\'un accord de remise (« Aut idem »). + Autoriser les captures d\'écran + Si vous autorisez les captures d\'écran, la dernière page que vous avez ouverte restera visible en arrière-plan lorsque vous changerez d\'application. Si nécessaire, vos données personnelles peuvent être visibles. Nous vous recommandons donc de ne pas autoriser les captures d\'écran. + Permettre + Annuler + Passez votre clavier aux autocollants ou utilisez des emojis pour votre photo de profil. + Choisir une photo de profil + Comment veux-tu continuer ? + Sélectionnez une photo + caméra + Émoji + Annuler + Ouvrir les paramètres + Utiliser + Prendre une photo + Éditer la photo de profil + Supposé il %s minutes + Accepté le %s + Je viens d\'accepter + Accepté à %s heures + ID de santé + Choisissez une assurance + Si l’inscription avec le Health ID ne se déroule pas comme prévu, veuillez suivre les conseils de notre centre d’aide. + Aide + Aide + Conseils pour vous inscrire à l’application d’assurance + Votre compagnie d’assurance est responsable du Health ID. Veuillez les contacter si vous avez des questions concernant l\'inscription. Voici quelques conseils éprouvés : + Veuillez noter qu\'en fonction de votre assurance, une application distincte peut être nécessaire. Veuillez vérifier auprès de votre compagnie d’assurance pour savoir de quoi il s’agit. + Démarrez l\'application de caisse et connectez-vous une fois avant de commencer à vous inscrire dans l\'application e-prescription. + Passer de l’enregistrement avec le Health ID à la carte de santé peut entraîner des problèmes. Par conséquent, veuillez d’abord vous déconnecter activement de votre profil avant de modifier l’option de connexion. + Si votre assurance ne figure pas dans la liste, vous pouvez également vous inscrire avec votre carte santé et le code PIN associé. + Si l’application de caisse ne vous redirige pas vers l’application e-prescription, veillez à signaler cette erreur à votre compagnie d’assurance. + Si l\'application d\'assurance n\'est pas accessible, il peut être utile d\'accéder aux paramètres du navigateur de votre smartphone et d\'autoriser l\'ouverture des liens. + Si votre smartphone fonctionne sous Android 14, il peut être utile d\'autoriser l\'ouverture des liens dans les paramètres. + Ouvrir les paramètres + cela n\'a malheureusement pas fonctionné + Veuillez réessayer plus tard. + Messages d\'erreur lors du chargement. + Veuillez choisir un emoji ou un texte + Sauvegarder + Merci de vous connecter pour continuer + Plus loin + Appel + Écrire\nMail + À la pharmacie + Pharmacie + Aujourd\'hui + Demain + Route\nici + Supprimer l\'ordonnance et le reçu de paiement + Si vous supprimez la recette, la facture de frais associée est également supprimée. + Les messages de tous les profils sont désormais affichés ensemble + Nouvelles fonctionnalités + Nouvelles fonctionnalités + Nous travaillons constamment sur de nouvelles fonctionnalités. Partagez votre opinion avec nous et aidez-nous à créer une meilleure application. + Des commandes pour tout le monde + Sous Commandes, vous pouvez désormais voir les commandes de tous les profils ensemble et les visualiser beaucoup plus facilement. + + Aucun remboursement de frais + En règle générale, l\'assurance maladie ne prend pas en charge les frais liés à cette prescription. En tant que patient, vous êtes donc responsable du paiement intégral. Il convient de vérifier individuellement si les frais seront remboursés dans le cadre d\'une assurance complémentaire ou de prestations légales. + Votre assurance ne prendra en charge aucun frais pour cette prescription. + + "Votre assurance ne couvrira pas l\'ordonnance %s " + "Votre assurance ne couvrira pas les ordonnances %s ." + + Causé + Date + Accident + Accident du travail + Maladie du travail + Nouvelles + Pas de nouvelles + Vous n\'avez pas encore de messages. + 🎉 Votre commande est prête à être récupérée. Veuillez montrer ce code de retrait pour vous identifier. + Malheureusement, le message de votre pharmacie était vide. Veuillez contacter votre pharmacie. + La pharmacie vous a fourni un lien. + Nouvelles + Commande + Messages + Pas de connexion Internet + Pour visualiser les appareils connectés, vous devez être connecté à la biométrie. + L\'ordonnance a été supprimée le %s + Supprimé + Aucune recette + Vous n\'avez aucune recette dans les archives. + Vos reçus de dépenses ont été supprimés + Reçu des coûts + Inscription requise + Veuillez vous connecter pour afficher les appareils connectés. + Registre + Registre + Connectez-vous à %s Assurance + Carte santé sur smartphone + Non connecté + Inscrit + rafraîchir + Vous recevrez des journaux d\'accès si vous êtes connecté au service de prescription. + Inscription requise + Veuillez vous connecter avec votre carte de santé et enregistrer vos informations de connexion avec biométrie pour visualiser les appareils connectés. + %1$s x Matin + %1$s x Déjeuner + %1$s x Soirée + %1$s x La nuit + Sauf indication contraire de votre médecin, la notice d\'utilisation peut être comprise comme suit : + Votre médecin vous a donné ces informations sur la prise du médicament. + Il n’y a aucune information sur la façon de prendre le médicament dans votre ordonnance. + Votre médecin a noté que vous aviez reçu des instructions sur la façon de prendre ce médicament qui ne figurent pas sur l\'ordonnance. Cela pourrait figurer sur votre plan de médicaments, par exemple. + Non spécifié + DJ + Mode d\'emploi + " %s - %s" + Rappel de revenus + Prendre des rappels + Médicaments oraux + À propos des rappels d\'admission + Matin + Midi + Après-midi + Le soir + médicament + Informations posologiques + 1 + %s / %s + 1 dose + Changer la posologie + Annuler + Rappel de médicaments + Calendrier des médicaments + Prendre la posologie selon la prescription du médecin + À %s heures, prenez %s x + Sélectionnez les dates + Plus loin + Annuler + UN + De + Suivi + Illimité + Individuellement + Premier jour + Dernier jour + Ajouter du temps + Aucun rappel d\'admission + Vous pouvez définir des rappels pour vos ordonnances + Allumé + Éteint + Souviens-toi de moi + Désactivez l\'optimisation de la batterie pour cette application. + Si vous n\'activez pas cette option, les rappels de médicaments peuvent sembler peu fiables. + Annuler + Permettre + Mode d\'emploi + Non spécifié + information + N\'oubliez pas non plus en mode optimisation de la batterie + Changer la posologie + Foule + formulaire + terminé + illimité + à %s + Répéter %s + Dans la mesure du possible, nous utilisons les informations stockées dans l\'ordonnance pour le calcul. + Annuler + Sauvegarder + Jusqu\'à la fin du pack + Temps + dose + Voir les médicaments + Avez-vous déjà pris vos médicaments ? + Prendre des rappels + Aucun souvenir actif + Vous n\'avez aucun rappel à prendre aujourd\'hui + Sélectionnez la date de début + Sélectionnez la date de fin + Accueillir + dans votre application e-prescription + Essayer à nouveau + Entrez le mot de passe + Veuillez vous assurer que toute personne avec qui vous pouvez partager cet appareil et qui connaît vos informations de connexion a également accès à vos recettes. + Entrez le mot de passe + Veuillez saisir le mot de passe pour déverrouiller l\'application. + mot de passe + Mot de passe + Veuillez saisir un mot de passe pour sécuriser l\'application. + Afficher le mot de passe + Répéter le mot de passe + Vous avez oublié votre mot de passe ? Veuillez supprimer l\'application, puis la réinstaller. Vous pouvez découvrir pourquoi dans notre %s. + Entrez le mot de passe + Le mot de passe doit comporter au moins huit caractères + La force du mot de passe n\'est pas suffisante + Force du mot de passe suffisante + Sauvegarde de l\'appareil impossible + Veuillez d\'abord configurer une sauvegarde de l\'appareil. + Annuler + Paramètres + Force du mot de passe très bonne + Sécurité des applications + Sauvegarde de l\'appareil + Veuillez choisir au moins une méthode pour sauvegarder l\'application. + mot de passe + Changer le mot de passe + Pour supprimer, une connexion au Prescription Server doit être établie + Les marchandises sont prêtes + Les marchandises sont prêtes depuis %s + Les marchandises sont prêtes à l\'instant + Les marchandises sont prêtes depuis %s minutes + Les marchandises sont prêtes depuis %s + Désactivé car aucun mot de passe n\'est saisi. + Désactivé car le mot de passe est trop faible. + Désactivé car les mots de passe ne correspondent pas. + Explorer + Registre des dons d\'organes + Ouvrir un registre de don d’organes ? + Vous serez redirigé vers le registre des donneurs d’organes. Afin de consulter et de modifier vos données de don d\'organes, vous devez vous y connecter. + Ouvrir + Annuler + Fonction non active en mode démo diff --git a/app/features/src/main/res/values-ga-rIE/strings.xml b/app/features/src/main/res/values-ga-rIE/strings.xml new file mode 100644 index 00000000..8ffcee8f --- /dev/null +++ b/app/features/src/main/res/values-ga-rIE/strings.xml @@ -0,0 +1,1219 @@ + + + ceart go leor + Cealaigh + Ar ais + ag + Digiteach. Go tapa. Sábháilte. + Aitheantas Tasc + Cód rochtana + Téarmaí Úsáide + Beartas príobháideachta + Oidis + Diúltaíodh rochtain ar cheamara + Chun an scanóir a úsáid, ní mór duit cead a thabhairt don aip rochtain a fháil ar do cheamara i socruithe an chórais. + Dírigh an ceamara ar chód oideas + Ní cód bailí oideas é seo + Tá an cód oidis seo scanadh cheana féin + + %s oideas aitheanta + + + + %s oideas aitheanta + + Cealaigh + Solas ceamara + Cealaigh an scanadh? + ceart go leor + Ná cealaigh + A ligean ar dul + Cad atá uait: + Cuir isteach uimhir rochtana + Cuir isteach PIN + Bain triail eile as + Theip ar nascadh leis an bhfreastalaí. + + Tá %s iarracht amháin eile agat sula gcuirtear bac ar do chárta le húsáid UAP. + + + + Tá %s iarracht eile agat sula gcuirtear bac ar do chárta le húsáid UAP. + + Gheobhaidh tú an uimhir rochtana i gcúinne uachtarach do chárta leighis ar thaobh na láimhe deise. + Cealaigh + Cárta á chuardach... + Coinnigh an cárta leighis ar chúl an fheiste + Fós ag cuardach ... + Bog an cárta ar chúl an ghléis go mall. + Leid + D\'fhéadfadh sé go mbeadh sé deacair ceangal a dhéanamh trí NFC i gcásanna feiste. + Cárta aitheanta + Déan iarracht gan an cárta leighis a bhogadh. + Cárta leighis aimsithe. Ná bog le do thoil. + Idirbhriseadh an nasc + Coinnigh do chárta leighis ar chúl an fheiste arís + Leagan: %s + Tóg hais: %s + Roghchlár dífhabhtaithe + Oscail go dtí %s a chlog + Oscail go leanúnach + Imprint + Foilsitheoir + gematic GmbH\nFriedrichstr. 136\n10117 Beirlín, an Ghearmáin + Bainistíocht: An Dr. Florian Fuhrmann, Brenya Adjei, an Dr Florian Hartge\nCúirt clárúcháin: An Chúirt Dúiche Beirlín-Charlottenburg\nCláruimhir tráchtála: HRB 96351\nUimhir aitheantais CBL: DE241843684 + Freagrach as an ábhar + Florian Fuhrmann, Brenya Adjei, an Dr Florian Hartge + Teagmháil + Nóta + Déanaimid ár ndícheall teanga inscne-íogair a úsáid. Má thugann tú aon earráidí faoi deara, bheadh ​​áthas orainn cloisteáil uait ar ríomhphost. + ardán nua-aimseartha na Gearmáine don leigheas digiteach + Scríobh ríomhphost + Oscail láithreán gréasáin + Díghlasáil + Clár + Cealaigh + Eolas dlíthiúil + Imprint + Beartas príobháideachta + Téarmaí Úsáide + Sonraí an oideas + Marcáil mar fhuascailt + Marcáil mar neamhfhuascailte + Foirm dosage + Méid an phacáiste + Duine árachaithe + Ainm + Seoladh + Dáta breithe + Árachas sláinte / aonad costais + Stádas + Uimhir sealbhóir polasaí + Oidiseoir + Ainm + Lia speisialtóra + Uimhir lianna (LANR) + Institiúid + Ainm + Seoladh + Uimhir bhunaíochta + Uimhir theileafóin + Seoladh ríomhphoist + Uimhir cuideachta timpiste nó fostóra + Ar mhaith leat an oideas seo a scriosadh go buan? + Scrios + Cealaigh + Uaireanta oscailte + Suíomh Gréasáin + Ní féidir é a fhuascailt ach inniu mar chustaiméir féiníocaíochta + Clár + Cumasaigh NFC + Cumasaigh an fheidhm NFC ar do ghléas chun logáil isteach le do chárta leighis. + Cumasaigh + Ceart + Oideasanna fuascailte? + Ar mhaith leat na hoideas a mharcáil mar oideas fuascailte? + Gan fhuascailt + Fuascailte + Osclaítear ar %s a chlog + +49 800 277 377 7 + Beolíne teileafóin + Oscail scanóir le haghaidh oidis + Socruithe + Scrios screenshots + An gceadaíonn tú do R-Oideas anailís a dhéanamh ar d’iompraíocht úsáide gan ainm? + Eolas teicniúil + Slándáil do shonraí oidis + Tabhair faoi deara le do thoil go bhféadfadh rochtain a bheith ag daoine ar féidir leat an gléas seo a roinnt leo agus a bhféadfadh a mbithmhéadracht a stóráil ar an ngléas seo rochtain a fháil ar d\'oideas freisin. + Theip ar sheoladh + Níl aon ríomhchlár ríomhphoist socraithe + Gan torthaí + Níorbh fhéidir linn aon toradh a aimsiú leis an téarma cuardaigh seo. + Ceadúnais foinse oscailte + Teagmháil + Glaoigh ar thacaíocht teileafóin + Suirbhé mar gheall ar an app + +49 800 277 377 7 + Ba mhaith liom cabhrú leis an aip a fheabhsú + Cuimsíonn sé seo faisnéis crua-earraí agus bogearraí do ghutháin, socruithe aipeanna ríomh-oideas agus an méid úsáide, ach ní do shonraí pearsanta nó sláinte riamh. + Is iad soláthraithe próiseála sonraí amháin a sholáthraíonn na sonraí do Gematik GmbH agus scriostar iad tar éis 180 lá ar a mhéid. Is féidir leat an anailís a dhíchumasú arís ag am ar bith tríd an roghchlár san app. + Ligeann na sonraí seo dúinn tuiscint a fháil ar na feidhmeanna a úsáidtear go minic agus iad a fheabhsú. Is féidir linn a mheas freisin cé chomh fada is a chaithfear tacú le teicneolaíocht níos sine agus cathain is féidir linn, mar shampla, leagan níos nuaí den chóras oibriúcháin a dhéanamh éigeantach gan cur isteach ar (an iomarca) úsáideoirí. + Feabhsaigh an app + Tá anailís gan ainm fós díchumasaithe + %s Go raibh maith agat as do thacaíocht! + Clár + Cuir in iúl duit féin le do thoil. + Nóta do chógaslanna: Faighimid sonraí teagmhála agus faisnéis faoi chógaslanna ó mein-apothekenportal.de. An bhfuil earráid aimsithe agat nó ar mhaith leat sonraí a cheartú? + Faigh tuilleadh eolais + Cógaslanna + Ar an drochuair níor oibrigh sé sin \uD83D\uDE15 + Bain triail eile as. + Áiseanna inrochtaineachta + Cumasaigh súmáil + Sábháil + Moltaí: %s + Scríobh ríomhphost + Aistrítear an fhaisnéis seo a leanas faoi na crua-earraí agus an córas oibriúcháin a úsáideann tú nuair a sheolann tú ríomhphost: + Fuascailt ar an suíomh amháin + Ní féidir leat ríomh-oideas a sheoladh chuig an gcógaslann seo go fóill. + Oscail anois + Seirbhís cúiréireachta + Loingsiú + Scagairí + Scagaire + Comhroinn suíomh + Comhaontaithe + Meaitseálann an pasfhocal arís agus arís eile + Earráid 20 10 76631 + Tá do theastas cárta sláinte neamhbhailí. B\'fhéidir go bhfuil do chárta imithe in éag? Déan teagmháil le do chuideachta árachais sláinte le do thoil. + Iarrachtaí logáil isteach nár éirigh leo + + Braitheadh %s iarracht logáil isteach nár éirigh leo. + + + + Braitheadh %s iarracht logáil isteach nár éirigh leo. + + Comharthaí + Comhartha rochtana + Comhartha SSO + Níl aon chomhartha rochtana ar fáil + níl aon chomhartha SSO ar fáil + chóipeáil go dtí an gearrthaisce + Cliceáil chun an comhartha a chóipeáil chuig an ngearrthaisce + Infhuascailte ach inniu + Ceadaigh + Níl aon cheangal leis an bhfreastalaí + Bain triail eile as i gceann cúpla nóiméad. + Athlódáil + Taispeáin comharthaí + Conas ba mhaith leat an aip seo a dhéanamh slán? + Nóta + Níl slándáil ghléis ar bith socraithe don ghléas seo + Molaimid duit cosaint bhreise a chur le do shonraí leighis trí do ghléas a shlánú mar shampla le cód nó le bithmhéadracht. + Ná taispeáin an teachtaireacht seo amach anseo. + Theip ar an gceangal. Níorbh fhéidir nasc líonra a chruthú. + Theip ar an gcumarsáid leis an bhfreastalaí: cód stádais %s. + Theip ar chumarsáid leis an bhfreastalaí: Seiceáil an nasc idirlín agus na socruithe ama/dáta. + Rabhadh + Seans go bhfuil slándáil laghdaithe ag do ghléas + Is féidir é seo a chur faoi deara, mar shampla, trí ghléasanna ionramhála nó nuair a chuirtear modh an fhorbróra ar siúl. Molaimid gan an aip a úsáid ar ghléasanna jailbroken ar chúiseanna slándála. + Admhaím an baol méadaithe agus ba mhaith liom leanúint ar aghaidh ar aon nós. + Cén fáth a bhfuil feistí le rochtain fréimhe ina mbaol slándála féideartha? + Faigh tuilleadh eolais + Ainm próifíle + Cuir isteach ainm don phróifíl nua. + Ainm próifíle + Próifílí + Conas cárta leighis atá cumasaithe ag NFC a aithint + Ní féidir teagmháil a dhéanamh tríd an aip seo + Déan teagmháil le do chuideachta árachais sláinte trí na gnáthbhealaí. + Cárta leighis & PIN + UAP amháin + Logáil isteach ar an aip ríomh-oideas + Ní féidir leis an réimse ainm a bheith folamh. + Tá próifíl leis an ainm seo ann cheana. + Próifíl + %s roghnaithe + Dath an chúlra + Liath an earraigh + Drúcht gréine + Tá sé! Tá! Bándearg! + Crann + Gealach gorm Meán Fómhair + Gan logáil isteach + Ceangailte + Ceangailte dheireanach ar %s + An bhfuil fonn ort próifíl a scriosadh? + Scriosfaidh sé seo na sonraí go léir ón bpróifíl ar an ngléas seo. Coinneofar d’oideas sa líonra sláinte. + Scrios + Cealaigh + Scrios próifíl + Ba mhaith leat an phróifíl dheireanach a scriosadh. + Éilíonn an app próifíl amháin ar a laghad. Cuir isteach ainm don phróifíl nua. + Earráid 20 10 76831 + Níorbh fhéidir clár na gcártaí leighis a bhaint amach. Bain triail eile as. + Is féidir leat teacht ar fhaisnéis atá deimhnithe go gairmiúil maidir le tinnis, cóid ICD agus saincheisteanna a bhaineann le cosc ​​agus cúram sláinte sa Tairseach Náisiúnta Sláinte. + Oscail gesund.bund.de + Tá an polasaí Príobháideachta leasaithe againn + Tá an aip ríomh-oideas tagtha chun cinn, agus mar sin bhí orainn ár bpolasaí Príobháideachta a nuashonrú. + Oscail polasaí príobháideachta + Tá sé seo athraithe ó %s : + Cad a tharlaíonn nuair a osclaíonn tú an aip? + Cad a tharlóidh má úsáidim an fheidhm ceamara/léamh oidis ag baint úsáide as an ceamara? + Níl aon oideas nua ar fáil + + %s oideas nua + + + + %s oideas nua + + infhuascailte + Tá sé á phróiseáil + Fuascailte + Anaithnid + Taispeáin logaí rochtana + Cé a fuair rochtain ar do oidis agus cathain? + Eochair rochtana ar an tseirbhís oidis + Logs rochtana + Uimh logs rochtana + Níl aon logaí rochtana ar fáil go fóill. + Tá an t-oideas á phróiseáil faoi láthair agus ní féidir é a scriosadh + Glac + Ní raibh an chuma air sin a bheith ag obair + Is eol dúinn go bhfuil na contúirtí ag baint leis an gceangal leis an gcárta sláinte. Amach anseo, ba cheart go bhféadfaí clárú freisin trí aip árachas sláinte atá fíordheimhnithe cheana féin.\n\nTáimid ag obair freisin chun a chinntiú gur féidir oidis a fhuascailt go digiteach gan chlárú.\n\nAr thug tú aon rud faoi deara le linn an phróisis seo ar mhaith leat a roinnt linn? Scríobh chugainn le do thoil, bheimis sásta aiseolas ríthábhachtach a fháil. + Leideanna ceangail + Méadú ar neart an cheangail + Bain an cás cosanta más gá. + Má chreathadh an gléas agus ansin briseadh as an nasc, lorg an suíomh idéalach laistigh de gha beag. + Ná bog an gléas go han-mhall thar an gcárta. + Cuir an gléas go díreach ar an gcárta. + Is féidir leat é sin a dhéanamh tríd an gcárta leighis a chur ar dhromchla cothrom (eg bord). + Méadú ar neart an cheangail + Tabhair faoi deara suíomh an bhraiteora NFC + Faigh amach cá bhfuil an braiteoir NFC ar do ghléas (mar shampla, seo forbhreathnú ar ghléasanna faoi %s ). + I gcásanna áirithe d’fhéadfadh suíomh an bhraiteora NFC a bheith éagsúil laistigh de shraith samhail (is iad seo na sonraí mar shampla don %s ). + An chéad leid eile + Ar aghaidh + Dún + Bain triail as + Déan teagmháil linn + Cuardach cógaisíochta a cheadúnú + Fhuascailt + Oideas scanta + Scanta ar %s + Marcáilte mar fhuascailt ar %s + Conas ar mhaith leat leanúint ar aghaidh? + Seol chuig an gcógaslann + Ar fáil go luath + Cuir in áirithe anois é le bailiú nó bíodh sé seachadta trí sheirbhís cúiréireachta nó seolta + Sábháil chun ordú níos déanaí + Sábháil oidis ar an ngléas + + Lean ar aghaidh le %s oideas + + + + Lean ar aghaidh le %s oideas + + Theip ar nascadh an chárta leighis + Tá an phróifíl reatha ceangailte le cárta leighis eile cheana féin (uimhir árachais sláinte %s ). + Tá do chárta leighis ceangailte le próifíl eile cheana féin. Athraigh go próifíl %s. + Sábháil + Sonraí teagmhála agus seoladh + Teagmháil + Uimhir theileafóin + Seoladh ríomhphoist (roghnach) + Seoladh seachadta + Céad ainm agus sloinne + Sráid agus uimhir tí + Líne seoltaí breise (roghnach) + Treoir seachadta (roghnach) + Tuilleadh sonraí teagmhála ag teastáil + An bhfuil fonn ort na hathruithe a scriosadh? + Caith amach + Úsáideann cuardaigh leis an eolaire cógaslanna geo-chomhordanáidí a chuirtear ar fáil le cúnamh OpenStreetMap. Gabhaimid buíochas leis an tionscadal seo as a gcabhair. + © OpenStreetMap ( %s ) + https://www.openstreetmap.org/copyright + Príobháideacht & Úsáid + Ar aghaidh + Bhí ort UAP do chárta sláinte a ordú go gníomhach ó do chuideachta árachais sláinte agus ansin é a fháil trí phróiseas slán ar nós Postident. + Ní bhfuarthas UAP + UAP + Seiceáil do nasc leis an Idirlíon agus socrú ama/dáta do ghléis. + Faoi ghlas amach? Déan athbhreithniú ar do shonraí rochtana bithmhéadracha ar an ngléas seo. + Crios cabhrach + Cainníocht agus aonad + Substaint ghníomhach + Cainníocht substainte gníomhaí + Cur síos ar bhaisc + Úsáid ag + Catagóir + Vacsaín + Glac + Cealaigh + Nóta + Cabhraigh linn an aip seo a fheabhsú + Tá pasfhocal le feiceáil + Níl pasfhocal le feiceáil + Bithmhéadracht + Pasfhocal + Ag feitheamh le freagra + Gan oideas + Níl aon oideas agat is féidir a fhuascailt faoi láthair. + Nuashonrú + Uathoibríoch logáil as + Déanfar do chuid a dhínascadh ón bhfreastalaí oideas 12 uair an chloig ar chúiseanna slándála. Athcheangail chun oidis reatha a fháil. + Ceangail + An bhfuil prionta páipéir faighte agat? + Cuir oidis le do liosta tríd an gcnaipe scanadh a thapáil sa chúinne uachtarach ar dheis. + Scanadh páipéar a phriontáil + Ní mór duit a bheith logáilte isteach chun oidis a fháil go huathoibríoch. + Clár + Gan aon oideas fuascailte + Tá do oideas fuascailte ar taispeáint anseo. Scriosfar d’oideas ón bhfreastalaí oidis tar éis 100 lá ar fhorais cosanta sonraí. + Gan aon oideas fuascailte + Tá do oideas fuascailte ar taispeáint anseo. Scan oidis nua chun tús a chur leis an bpróiseas fuascailte. + Bainistíocht gléas + Gléasanna nasctha + Cláraithe ó %s (an gléas seo) + Cláraithe ó %s + Ar chúiseanna slándála, cuirtear deireadh leis an gceangal leis an bhfreastalaí oidis tar éis 12 uair an chloig. Chun athcheangal a dhéanamh, beidh cárta sláinte agus UAP uait le haghaidh gach próiseas nasctha. + UAP + Cuir isteach do PIN (cárta leighis). + Ar aghaidh + Clár + Gléasanna nasctha + An bhfuil fonn ort an gléas a bhaint? + Cealaigh + Bain + An bhfuil fonn ort an gléas seo a bhaint? + Ar mhaith leat %s a bhaint ? + Má bhaineann tú %s, gearrfar an ceangal leis an bhfreastalaí oidis go buan i gceann 12 uair an chloig ar a mhéad. + Gléasanna á lódáil... + Gan feistí + Níl aon fheistí bainteach leis an gcárta leighis seo. + Bain triail eile as + Ach :-( + Níorbh fhéidir liosta na ngléasanna a lódáil. + Gan ceangal + Gan nasc Idirlín. + Cógais agus cóirithe + Támhshuanaigh + Eisiúint leigheasanna ar oideas amháin de réir alt 4 AMVV + An bhfuil aon chabhair uait? + Tá roinnt leideanna curtha le chéile againn chun na fadhbanna is coitianta a réiteach. + Seoladh leideanna ceangail + Díghlasáil + Cárta bactha + Cuireadh an PIN isteach go mícheart trí huaire. Mar sin cuireadh bac ar do chárta le húsáid le PIN ar chúiseanna slándála. + Díghlasáil cárta + Cuir isteach PUK + Le do PIN tá PUK 8 ndigit faighte agat ó do chuideachta árachais. + Roghnaigh UAP nua + Is féidir leat do UAP nua a roghnú tú féin (6 go 8 ndigit). + An cuimhin leat UAP? + Déan nóta de do PIN agus coinnigh in áit shábháilte é. + Cealaigh + ceart go leor + Ní féidir an scaoilfeadh glas ar + D\'úsáid tú an PUK seo chun do chárta a dhíghlasáil an t-uaslíon uaireanta nó chuir tú isteach é go mícheart arís agus arís eile. Déan teagmháil le do chuideachta árachais sláinte le do thoil. + Is féidir leat PUK amháin a úsáid ar feadh suas le 10 n-oscailt. + cárta díghlasáilte + Cad atá uait: + Do chárta leighis + Cárta leighis PUK + Ar aghaidh + Cárta árachais + Ordaigh PIN nó cárta + Clár + Conas ba mhaith leat a fhíordheimhniú? + Cárta leighis atá cumasaithe ag NFC + PIN cárta leighis + Nach bhfuil cárta leighis agus PIN atá cumasaithe ag NFC agat fós? + Ordú anois + Nó: Sínigh isteach le do %s. + app cuideachta árachais sláinte + msgstr \"Tá uimhir rochtana do chárta suite sa chúinne uachtarach ar thaobh na láimhe deise ar thaobh tosaigh do chárta leighis.\" + Níl uimhir rochtana ar mo chárta + + Tá %s iarracht amháin eile agat sula gcuirtear bac ar do chárta le húsáid UAP. + + + + Tá %s iarracht eile agat sula gcuirtear bac ar do chárta le húsáid UAP. + + Cuir cárta leighis ar chúl an ghutháin + Féadfaidh an próiseas seo a leanas suas le 30 soicind a ghlacadh. + Cuir cárta %s ar chúl an ghutháin. + sa limistéar uachtarach ar dheis + sa limistéar lárnach uachtarach + sa limistéar uachtarach ar chlé + sa limistéar lárnach ar dheis + go lárnach + sa limistéar láir ar chlé + sa limistéar íochtair ar dheis + sa limistéar lárnach íochtair + sa limistéar íochtair ar chlé + Cabhrú + Seoladh %s nóiméad ó shin + Seolta ar %s + Seolta díreach anois + Seolta ag %s a chlog + Níl sé bailí a thuilleadh + Aiseolas ón aip ríomh-oideas + Táimid ag tnúth le do chuid aiseolais. Bain úsáid as an spás thíos le do thoil agus cuir do thuairimí in iúl chomh beacht agus is féidir: + PUK + Dún + Is mór an trua... + Ar an drochuair, ní chomhlíonann do ghléas na ceanglais íosta chun logáil isteach san aip ríomh-oideas. Chun fíordheimhniú slán a dhéanamh le do chárta leighis, tá ar a laghad Android 7 agus sliseanna NFC ag teastáil. + Faigh tuilleadh eolais + Sábháil sonraí logáil isteach? + Sábháil + Ná sábháil + Nóta + Ar chúiseanna slándála, cuirtear deireadh leis an gceangal leis an bhfreastalaí oidis tar éis 12 uair an chloig. Chun athcheangal a dhéanamh, beidh cárta sláinte agus UAP uait le haghaidh gach próiseas nasctha. + Socraigh cosaint bithmhéadrach + Ní féidir sonraí rochtana a shábháil. Roimh ré, socraigh slándáil bhithmhéadrach (m.sh. méarloirg) ar do ghléas. + Cealaigh + Socruithe + Nóta + Glac + Slándáil do shonraí oidis + \Úsáideann an aip seo an braiteoir bithmhéadrach is sláine a sholáthraíonn do ghléas chun do dhintiúir a dhéanamh slán i limistéar cosanta de stóráil an ghléis.\ + Má úsáideann tú cosaint bhithmhéadrach do do shonraí rochtana, is féidir leat an aip seo a sheoladh amach anseo gan do chárta leighis agus PIN chun oidis a fheiceáil, a aisghabháil, a fhuascailt nó a scriosadh. + Tabhair faoi deara le do thoil go bhféadfadh rochtain a bheith ag daoine ar féidir leat an gléas seo a roinnt leo agus a bhféadfaí a mbithmhéadracht a stóráil ar an ngléas seo ar do oidis. + Ar an drochuair níor oibrigh sin + Níor éirigh leis an bhfíordheimhniú le haip na cuideachta árachais sláinte. + Chuaigh sé in éag ar %s + Scriosadh an t-oideas ón bhfreastalaí cheana féin + Ceartaigh d’iontráil nó caith na hathruithe amach + Ceart + Sonraí sealbhóir polasaí + Ainm + Árachas + Uimhir sealbhóir polasaí + FÉIDIR + Clár + Logáil amach + Sábháil + Athrú + Cuir pictiúr próifíle in eagar + Ar aghaidh + Níl an freastalaí ag freagairt + Bain triail eile as ar ball. + Bain triail eile as + Cuardaigh le haghaidh árachais + Ceangail leis an bhfreastalaí oidis anois? + D\'éirigh le logáil isteach + Ceangal caillte + Ceangail leis an bhfreastalaí oidis anois? + Gan comharthaí + Gheobhaidh tú comhartha nuair a bheidh tú logáilte isteach sa tseirbhís oidis. + Roghnaigh PIN atá uait + Díghlasáil Cárta + Roghnaigh PIN + UAP arís + Tá na hiontrálacha difriúil óna chéile. + Uimh orduithe + Níl aon orduithe agat fós. + Díreach anois + Ag %s chlog + Tá cart siopadóireachta réidh + Cuireadh an t-oideas le do thralaí siopadóireachta. Téigh go dtí láithreán gréasáin na cógaisíochta chun an t-ordú a chomhlánú. + Oscail cart siopadóireachta + Taispeáin an cód bailiúcháin seo ag an gcógaslann. + Faigh cód piocadh + Ní féidir an teachtaireacht a thaispeáint + Déan teagmháil le do chógaslann ( %s ). + Nasc cógaisíochta + Taispeáin cód bailithe + Taispeáin an teachtaireacht + %s ag %s chlog + Oideas seolta chuig %s . Níl rogha freagartha digiteach ag roinnt cógaslanna go fóill. Mura bhfaighidh tú freagra faoin lá amárach, glaoigh orainn mar réamhchúram. + Forbhreathnú ordú + Nua + Cúrsa + Ordú + Saor in aisce a ar an té atá ag glaoch. Amanna seirbhíse: Luan - Aoine 8:00 am - 8:00 pm ach amháin ar laethanta saoire cónaidhme + Cógaisíocht + Roghnaigh PIN atá uait + Sábháladh an UAP inmhianaithe + Faoi láthair oscailte agus in aice liom + Scag de réir … + Cuardach + Sannadh díreach + Cógaslanna + Uimhir ghutháin (roghnach) + Cuardach + Níl aon eolas bailí cógaslainne + Ní bhfuarthas aon fhaisnéis reatha faoin gcógaslann seo. Scriosfar an iontráil don chógaslann seo. + ceart go leor + Níl an t-eolaire cógaslainne ar fáil + Ní féidir teacht ar aon fhaisnéis faoin gcógaslann seo. Seiceáil do nasc idirlín. + Cealaigh + Bain triail eile as + Sábháil Comhshaol + Ní féidir logáil isteach + Dealraíonn sé go bhfuil do shaintréithe logáil isteach bithmhéadracha athraithe. Logáil isteach arís le do chárta sláinte. + Cealaigh + Clár + Próifíl 1 + In aice liom + infhuascailte níos déanaí + Infhuascailte ó %s + Feabhsuithe táirge + Anailís gan ainm + Cabhraigh linn an aip seo a fheabhsú. Bailítear na sonraí úsáide go léir gan ainm agus úsáidtear iad chun taithí an úsáideora a fheabhsú amháin. + Socruithe pearsanta + Áiseanna inrochtaineachta + Feabhsuithe táirge + Cuireadh oideas leis + Oideas allmhairithe cheana féin + Tharla earráid le linn iompórtáil + FÉIDIR + Scrios + Oideas scanta + UAP Dearmadta + + %s Oideas + + + + %s Oideas + + Tá an polasaí príobháideachta agus na téarmaí úsáide léite agam agus glacaim leo. + Beartas príobháideachta + Téarmaí Úsáide + Déanfaimid: + Feabhas a chur ar inúsáidteacht. + Braith earráidí agus tuairteanna. + Ar ndóigh, bailigh na sonraí go léir gan ainm. + Is féidir leat an cinneadh seo a mhodhnú i socruithe an chórais am ar bith + Lean ort + Glac + Úsáideann an aip an modh is sábháilte atá socraithe agat ar do ghléas. + Sábháil + Roghnaigh + Cógas + Ainm trádála + + Níl + Dosage + Dáta eisiúna + Déanfar an t-oideas seo a fhuascailt duit mar chuid de chóireáil. + Gan sonraithe + Íocaíocht bhreise + Cógas + Treoracha le haghaidh úsáide + Incháilithe de réir BVG + Ullmhúchán malartach + Ainm an oideas + Pacáistiú + Treoracha déantúsaíochta + Cur síos + tugtha ag + eisithe ar: + Substaint ghníomhach + Forordaithe + Faigh + Cad is tasc díreach ann? + Le tarchur díreach, líontar oideas ó chleachtas nó ó ospidéal go díreach ag cógaslann. Ní gá do dhaoine árachaithe aon bheart a dhéanamh agus ní féidir leo idirghabháil a dhéanamh sa phróiseas fuascailte.\n Liostaítear atreoruithe díreacha san aip ríomh-oidis chun do chóireáil a dhéanamh níos trédhearcaí duit. + Táille seirbhíse éigeandála + Má dhéantar oideas a fhuascailt idir 8 pm agus 6 am nó ar an Domhnach agus laethanta saoire poiblí, féadfar táille bhreise de 2.50 euro a ghearradh. + Cógais atá faoi réir comhíocaíochta + Díolúine ó íocaíocht bhreise + Is gnách go n-íocann daoine a bhfuil árachas reachtúil acu uasmhéid de 10 euro ar dhrugaí ar oideas. Féadfaidh táillí níos airde a bheith i gceist má iarrtar druga ó mhonaróir sonrach nach bhfuil clúdaithe ag conradh lascaine leis an gcuideachta árachais sláinte (\"druga inmhianaithe\").\n Tá leanaí agus daoine óga faoi 18 saor ó íocaíochtaí breise.\n Má dhéantar oidis a fhuascailt níos déanaí ná 28 lá tar éis iad a bheith eisithe, ní mór don othar na costais a íoc ina n-iomláine.\n Má tá costais chógais ard i rith na bliana, is féidir díolúine ó chomhíocaíocht a iarraidh ar an gcomhlacht árachais sláinte + Tá tú díolmhaithe ó chomhíocaíocht a íoc as an gcógas seo. Clúdóidh do chuideachta árachais sláinte costas an chógais. + Cé chomh fada is atá an oideas seo bailí? + Le linn na tréimhse seo, is féidir leat d’oideas a fhuascailt in aon chógaslann le huasíocaíocht bhreise de €10. + Oideas scanta + Ní féidir le hoideas a allmhairítear ó chóip chrua faisnéis phearsanta nó leighis a thaispeáint ar chúiseanna slándála.\n Logáil isteach san aip seo le cárta sláinte nó aip árachais chun an fhaisnéis go léir atá san oideas a fheiceáil. + Oideas mícheart + Eisíodh an oideas seo go mícheart. + Táille seirbhíse éigeandála + Dosage de réir treoracha scríofa + Fón + Suíomh Gréasáin + Ríomhphost + Roinn do shuíomh chun teacht ar chógaslanna in aice leat. + ceart go leor + Cuir isteach UAP reatha + UAP mícheart curtha isteach + UAP reatha do chárta sláinte + Cárta bactha + Bain an bac de do chárta i Socruithe > Bain an bac de chárta. + Ar chúiseanna slándála, cuir isteach do UAP reatha. + Dearmad ar PIN + Oideas lochtach + Cógas + Is cosúil go ndeachaigh rud éigin mícheart agus d’oideas á chruthú agat. An bhfuil fonn ort earráid a thuairisciú? + Tuairisc + Gan logáil isteach + Cláraithe le + Cárta árachais + Bithmhéadracht + Gan logáil isteach + Fógra rabhaidh + Chuir an chógaslann le ceanáin + Baineadh an Chógaslann ó Cheanáin + Mo chógaslanna + Scríobh oibríocht nár éirigh leis + Níorbh fhéidir PIN a shábháil + Tuairisc + Sann UAP + Sáraíodh an riail rochtana + Níl cead agat rochtain a fháil ar an eolaire léarscáileanna. + Sann do bioráin féin + Tá an cárta daingnithe le PIN ó do chuideachta árachais sláinte (PIN iompair). Cuir isteach do UAP féin. + Pasfhocal gan aimsiú + Níl aon phasfhocal stóráilte ar do chárta. + Tá tú logáilte amach + Sínigh isteach arís chun d\'oideas a nuashonrú. + Uimhir an chomhábhair ghníomhacha + Cumas agus Aontacht + Fuasclaíodh %s nóiméad ó shin + Fuascailte ar %s + Fuascailte díreach anois + Fuascailte ag %s a chlog + Fuasclaíodh an t-oideas seo duit mar chuid de chóireáil. + Táille seirbhíse éigeandála + Ní féidir an t-oideas seo a líonadh ag cógaslann san oíche gan táille seirbhíse éigeandála breise a íoc. + Cuardaigh anseo + Socruithe + Roinn suíomh sna Socruithe. + In aice liom + Brúigh agus coinnigh chun an t-ainm a chur in eagar. + Cuir isteach an t-ainm nua don phróifíl. + Chun oidis a fháil go digiteach ó do chleachtas, ní mór duit a bheith logáilte isteach. + An bhfuil tú ag fáil oidis go digiteach? + Tarraing anuas ar an scáileán a athnuachan. + Gan oideas + Cláraigh chun oidis a fháil go huathoibríoch. + Clár + Cartlann Oideas + B\'fhéidir níos déanaí + Clár + Cuir pictiúr próifíle in eagar + Cartlann Oideas + Cuir isteach ainm + Sábháil + Mo ordú + Faighteoir: i + Oidis + Cógaisíocht + Seol + Athrú + Pioc suas ag an gcógaslann + Seachadadh trí chúiréireachta + Seachadadh tríd an bpost + %s Oideas + Ní féidir a fhuascailt + Níorbh fhéidir oideas amháin nó níos mó a fhuascailt. + Níor roghnaíodh aon oideas + Chun oidis a fhuascailt, ní mór oideas amháin ar a laghad a roghnú. + Cuir sonraí teagmhála leis + Athrú + Gan oideas + Níl aon oideas infhuascailte agat faoi láthair + Piocadh + cúiréireachta + Ordú poist + Roghnaigh oidis + Tapáil anseo chun oidis a scanadh + Brúigh fhada chun ainmneacha a chur in eagar + Cuir tuilleadh próifílí leis, m.sh. do do leanaí nó do thuismitheoirí + Cliceáil ar an taispeáint chun an leid uirlisí a thaispeáint. + Conas a fhuascailt? + Cén chaoi ar mhaith leat do chógas a fháil? + Taispeáin cód + Faigh scanadh ag an gcógaslann + Seol chuig an gcógaslann + Cuir in áirithe é nó tabhair é + Déanta + Cód bailiúcháin + Cóid aonair + + Tá %s oideas agat. + + + + Tá %s oideas agat. + + Déan rogha + Gach oideas + Cé na oidis? + Ar aghaidh + Ar aghaidh + Faigh tuilleadh eolais + Nóta + Úsáideann an aip seo bogearraí ó Google chun cóid a aithint. + Faigh tuilleadh eolais + Maidir leis an scanóir cód oideas + Cad iad na sonraí atá sa chód oidis? + Níl sa chód oideas ach aitheantóir an oideas. Ligeann sé seo an t-oideas a fháil ar an tseirbhís oidis sa líonra digiteach sláinte. Níl aon sonraí fút féin nó faoi do chógas sa chód oideas. + Mar sin ní féidir le duine ar bith aon rud a dhéanamh leis an gcód oideas amháin? + Ceart. Ní mór na sonraí oidis a íoslódáil ón tseirbhís oideas. Teastaíonn logáil isteach slán chuige seo. + Cé atá in ann clárú don tseirbhís oidis? + Is féidir le daoine árachaithe, cógaslanna, cleachtais agus ospidéil clárú don tseirbhís oidis sa líonra digiteach sláinte. + Cén fáth a n-úsáideann an aip ríomh-oideas gnéithe Google? + Tairgeann Google feidhmeanna ar féidir iad a chomhtháthú go héasca in aipeanna agus atá á bhforbairt agus á nuashonrú i gcónaí ag Google. Cinntíonn sé seo go n-oibríonn na feidhmeanna ar go leor gléasanna deiridh éagsúla agus is féidir iad a oibriú go slán. Úsáideann an aip feidhm chun an fheidhm ceamara agus scanadh do ghléasanna Android a fheabhsú (Google ML Kit). + Conas a oibríonn feabhsú scanadh le Google ML Kit? + Cuidíonn Google ML Kit le leas iomlán a bhaint as an íomhá a gabhadh ag ceamara ionas gur féidir na cóid oideas a léamh fiú i gcoinníollacha soilsithe bochta nó le samhlacha ceamara níos sine. + An roinnfear sonraí faoin oideas nó faoi mo chógas le Google? + Sábhálfar an cód oideas léite go díreach san aip. Ní chuirfear ar aghaidh chuig Google é. Ní stóráiltear na sonraí oidis sa chód, ach sa líonra digiteach sláinte. Ón áit sin seoltar chuig an aip iad. Níl rochtain ag Google ar an líonra sláinte digiteach. + Cad iad na sonraí a phróiseálann Google agus ML Kit á úsáid? + Níl rochtain ag Google ach ar fhaisnéis theicniúil faoin bhfeiste deiridh a úsáidtear agus faoi úsáid ghinearálta na feidhme breise (m.sh. ráta earráide, socruithe ceamara) chun é seo a thaifeadadh go staitistiúil agus mar sin an fheidhm bhreise a fheabhsú. Nuair a dhéanann tú rochtain, déanann Google seoladh IP do ghléis deiridh a thaifeadadh go sealadach. Ní dhéanfaidh Google faisnéis fút agus a bhfuil san oideas a thaifeadadh. + An bhfuil úsáid Google ML Kit deonach? + Tá. Mar sin féin, tá ML Kit ionsuite sa scanóir cód oidis sa leagan Android den aip ríomh-oideas. Má úsáideann tú an scanóir cód oidis ar ghléas Android, úsáidtear an fheidhm ML Kit i gcónaí freisin. Mar sin féin, is féidir leat a dhéanamh gan an scanóir cód oideas a úsáid. Is féidir d’oideas a lódáil isteach san aip freisin má chláraíonn tú leis an líonra sláinte digiteach leis an gcárta sláinte leictreonach nó trí d’aip árachais sláinte. + An féidir liom a fheiceáil cé a chonaic mo oidis? + Tá. Tá gach rochtain ar do shonraí logáilte go hiomlán sa líonra digiteach sláinte. San aip ríomh-oideas is féidir leat a fheiceáil cé a fuair rochtain ar do shonraí. + Cá háit ar féidir liom dul i dteagmháil má tá ceisteanna agam faoin aip nó faoin ríomh-oideas? + Is féidir faisnéis mhionsonraithe a fháil sa dearbhú um chosaint sonraí. + Líon na bpacáistí forordaithe + Gan oideas + Ní mór duit oidis infhuascailte chuige seo. + Roghnaigh cuideachta árachais + Cuardaigh le haghaidh árachais + Cealaigh + Cad ba mhaith leat iarratas a dhéanamh air? + Ní mór duit cárta agus an PIN comhfhreagrach a bheith agat don aip seo. + Cén chaoi ar mhaith leat dul i dteagmháil le do chuideachta árachais? + Tairgeann do chuideachta árachais na roghanna teagmhála seo a leanas + Tairgeann do chuideachta árachais an rogha teagmhála seo a leanas + Dún + UAP curtha isteach go mícheart. + Uimhir rochtana curtha isteach go mícheart + PUK isteach go mícheart. + Admhálacha costais + Féach ar admhálacha costais + Admhálacha costais + Chun admhálacha costais a fháil, ní mór duit a bheith ceangailte leis an bhfreastalaí. + Ceangail + Gan admhálacha costais + Díchumasaigh + Cealaigh + Feidhm a dhíghníomhachtú + Scriosfaidh sé seo gach admháil speansais ón ngléas seo agus ón bhfreastalaí. + Admhálacha costais a fháil + Déantar d’admhálacha costais a shábháil ar an bhfreastalaí oidis freisin. + Cumasaigh + Iomlán: %s %s + Roghnaigh + Scoilt + Scrios + Scrios + Cuir isteach + %s € + Praghas iomlán + Leid: Cuir admhálacha costais isteach tríd an aip árachais + Cuir isteach admhálacha costais go héasca trí aip do chuideachta árachais. Sa chéad chéim eile, roghnaigh an app agus brúigh Roinn. + Cleachtadh + Cógaisíocht + Dáta + Taispeáin níos mó + ID drugaí + Eisithe le haghaidh + KVNR: %s + Rugadh ar: %s + ceart go leor + Conas a chuireann tú doiciméid tacaíochta isteach? + Aistrigh go díreach chuig aip d\'oifig árachais/sochair. Chun seo a dhéanamh, roghnaigh an app ar an gcéad leathanach eile. + + Sábháil an comhad agus iompórtáil isteach sa tairseach árachais/sochair ar ball é. + Airteagal: %s + Comhaireamh: %s + CBL: %s %% + Praghas comhlán in EUR: %s + Táillí breise + Táille seirbhíse éigeandála + Táille BTM + Táille T-oideas + Costais soláthair + cúiréireachta + Iomlán in EUR: %s + Dualgas + Cinnte, scrios? + Scriosfar an comhad ó do ghléas agus ón bhfreastalaí. + Scrios + Seolta + Cód poist + Áit + Beidh a fhuascailt ar do shon + Fuasclaíodh ar do shon + Ní mór duit a bheith logáilte isteach chun an tseirbhís seo a úsáid. + Cárta árachais + UAP gaolmhar ag teastáil + Ní féidir é a fhuascailt ach amárach mar fhéiníocóir + Níl ach %s lá fágtha le fuascailt mar fhéiníocóir + Fós féin, infhuascailte mar fhéiníocóir ar feadh %s laethanta + Níl ach %s lá fágtha le fuascailt + Fós infhuascailte ar feadh %s lá + Ní féidir a fhuascailt ach amárach + Tá táillí i gceist + Glacann árachas + Aistríodh an t-oideas/na hoideas go rathúil. + Ní féidir an t-oideas a phróiseáil. Bain triail eile as. Seans go mbeidh ort cógaslann eile a roghnú. + Ní féidir an t-oideas a phróiseáil. Tuairiscíonn an chógaslann earráid anaithnid. Más gá, bain triail as cógaslann eile. + Dhiúltaigh an chógaslann an t-oideas. D’fhéadfadh an t-oideas a bheith neamhbhailí nó d’fhéadfadh do sheoladh seachadta nó d’fhaisnéis teagmhála a bheith neamhbhailí. + Bain triail eile as agus b\'fhéidir cógaslann eile a roghnú. Má leanann an earráid, téigh i dteagmháil leis an bhfoireann tacaíochta. + Aistríodh an t-oideas go rathúil. Mar sin féin, tuairiscíonn an chógaslann earráid phróiseála. Déan teagmháil leis an gcógaslann le do thoil. + Dhiúltaigh an chógaslann an t-oideas. Tá an t-oideas fuascailte cheana féin. + Dhiúltaigh an chógaslann an t-oideas. Tá an oideas scriosta. + Níorbh fhéidir an t-oideas a aistriú. Seiceáil do nasc idirlín agus bain triail eile as. + Níorbh fhéidir oideas amháin nó níos mó a aistriú. + Earráid á sheoladh + Seolta go rathúil! + Earráid sa chógaslann + Earráid sa chógaslann + Déan teagmháil le cógaslann + Oideas fuascailte cheana féin + Scriosadh an t-oideas + Gan Idirlíon + Chun logaí rochtana a fháil, ní mór duit a bheith ceangailte leis an bhfreastalaí. + Is féidir leat an t-oideas a líonadh fós ag cógaslann laistigh den tréimhse seo, ach beidh ort an praghas ceannaigh iomlán don chógas a íoc leat féin. Nó is féidir leat iarraidh ar do chleachtas an t-oideas a atheisiúint. + Déanta + Iarr ar cheartú + Ag an gcógaslann + Sa app + Déan an cód seo a scanadh ag do chógaslann. + Iarratas ar cheartú billeála + Cógas + Cuir isteach 1 charachtar ar a laghad. + Nó: Bain triail as an app i mód taispeána + Mód taispeána + Mód taispeána + Úsáid mód taispeána + Mód taispeána cumasaithe + Críoch anseo + Gníomhachtaigh mód taispeána + Ní anois + Cuardach + Faigh admhálacha costais go digiteach + Nuair a bheidh na hadmhálacha costais curtha i ngníomh, gheobhaidh tú anseo iad tar éis duit d’oideas a fhuascailt. + Cumasaigh + Admháil costas digiteach + Chomh luath agus a bheidh an admháil costais taiscthe ag an gcógaslann, beidh sé le feiceáil anseo. + Admháil costais + Faigh admhálacha costais go digiteach + Nóta: Ní bhfaighidh tú d’admhálacha costais mar asphrionta ag an gcógaslann a thuilleadh. + Cumasaigh + B\'fhéidir níos déanaí + Comhaontú + Le do thoiliú, staonfaidh tú ó iad a phriontáil amach sa chógaslann agus cuirfidh tú d’admhálacha costais isteach go digiteach amach anseo. + Cealaigh + Comhaontaithe + Scoir mód taispeána + Chun forbhreathnú ar na fáltais chostais + Chun na fáltais chostais + Faigheann tú admhálacha costais go digiteach anois + Gheobhaidh tú admhálacha costais go digiteach cheana féin + ceart go leor + Bain triail eile as + Feidhm gníomhachtaithe + Is féidir leat admhálacha costais a fháil go digiteach cheana féin. + Gan Idirlíon + Níl aon nasc leis an Idirlíon. + Iarratas mícheart + Bhí fadhb leis an iarratas. Oibrímid ar réiteach. + Níl an freastalaí ag freagairt + Bain triail eile as i gceann cúpla nóiméad. + Theip ar nascadh + Ní féidir aon eolas a fháil faoi láthair. Bain triail eile as ar ball. + Diúltaithe + Dhiúltaigh an freastalaí d\'iarratas. Bain triail eile as ar ball. + Nuashonraigh an app + Chun an ghné seo a úsáid, nuashonraigh d\'aip le do thoil. + Theip ar logáil isteach + Bhí fadhb ag an bhfreastalaí logáil isteach. Sínigh isteach arís le do thoil. + Clár + Cóipeáil URL + Ceangail le haip árachais sláinte sheachtrach. + Ní féidir an aip árachas sláinte a aimsiú + Níorbh fhéidir an aip árachas sláinte a rochtain. Níor éirigh leis an bhfíordheimhniú. + Ní féidir leis an árachas sláinte seo + Cuir isteach uimhir theileafóin chun críocha teagmhála. + Cuir isteach céad ainm agus sloinne chun críocha teagmhála. + Cuir isteach uimhir sráide agus tí chun críocha teagmhála. + Tabhair do chód poist le do thoil chun teagmháil a dhéanamh linn. + Cuir in iúl d’áit chónaithe le do thoil chun teagmháil a dhéanamh linn. + Tá an uimhir theileafóin a d\'iontráil tú neamhbhailí. (róghearr, rófhada, carachtair neamhbhailí) + Cuir isteach seoladh ríomhphoist chun teagmháil a dhéanamh linn + Tá an seoladh ríomhphoist a iontráladh neamhbhailí. + Tá an téacs a d\'iontráil tú neamhbhailí. (carachtair rófhada, neamhbhailí) + Tá an téacs a d\'iontráil tú neamhbhailí. (carachtair rófhada, neamhbhailí) + Tá an téacs a d\'iontráil tú neamhbhailí. (carachtair rófhada, neamhbhailí) + Tá an téacs a d\'iontráil tú neamhbhailí. (rófhada, ró-ghearr, carachtair neamhbhailí) + Tá an téacs a d\'iontráil tú neamhbhailí. (carachtair rófhada, neamhbhailí) + Tá an téacs a d\'iontráil tú neamhbhailí. (carachtair rófhada, neamhbhailí) + Sonraí míchearta - ceartú ag teastáil + Aip bhreise ag teastáil + Molta + ID sláinte digiteach + Coigeartaigh socruithe? + Cumasaigh oscailt naisc i do shocruithe chun leanúint ar aghaidh. + Oscail socruithe + Leagan app as dáta + Tá do leagan den aip as dáta agus ní thacaítear leis a thuilleadh. Chun leanúint ar aghaidh leis an ríomh-oideas, nuashonraigh an aip le do thoil. + Nuashonrú + Tá d\'uimhir rochtana %s dhigit fada. + Is féidir le do UAP a bheith idir %s agus %s digit ar fad. + Pioc suas + cúiréireachta + Loingsiú + Fón cliste NFC-chumasaithe ag teastáil + Níorbh fhéidir linn an t-iarratas chun an t-oideas a scriosadh a phróiseáil. Táimid ag obair ar réiteach ( %s ). + Tharla earráid agus m\'oideas á scriosadh, fuair mé an cód earráide %s . Cuir in iúl dom le do thoil nuair is féidir scriosadh arís. + Ar aghaidh + Iarratas mícheart + Tuairisc + Scrios go háitiúil + Gan Idirlíon + Níorbh fhéidir an t-oideas a scriosadh. Seiceáil do cheangal idirlín agus bain triail eile as. + Bain triail eile as + ceart go leor + Ath-logáil isteach + Ní mór duit a bheith logáilte isteach chun an t-oideas a scriosadh (401). + Logáil isteach + Cealaigh + Scriosadh dodhéanta + Níl cead agat an t-oideas a scriosadh faoi láthair toisc go bhfuil sé ag an gcógaslann le líonadh nó gur sannadh díreach atá ar feitheamh (403) é. + An iomarca iarratas + Tá iarracht déanta agat an t-oideas a scriosadh rómhinic. Bain triail eile as ar ball (429). + Scrios oideas + Úps... + msgstr \"Tharla earráid freastalaí gan súil leis. Bain triail eile as níos déanaí (500).\" + Ní féidir slándáil app. Roimh ré, socraigh slándáil bhithmhéadrach (m.sh. méarloirg, ID aghaidh) ar do ghléas. + Ní féidir cúltaca bithmhéadrach toisc nach bhfuil do ghléas ag tacú leis. + Aird! Níl do ghuthán cosanta go maith. + Déantar do shonraí logáil isteach a shábháil ar do ghuthán. Chun rochtain a chosaint, úsáidtear an modh logála isteach is fearr leat. Níl an modh logála isteach seo, m.sh. patrún svaidhpeála, an-slán. Úsáideann tú an ghné “Sábháil sonraí logáil isteach” ar do phriacal féin. + Má úsáideann tú an fheidhm \"Sábháil sonraí logáil isteach\" fós, beidh tú in ann oidis leis an aip ríomh-oidis a fheiceáil, a rochtain, a fhuascailt nó a scriosadh amach anseo gan cárta sláinte agus an PIN a chur isteach. + Suíomh gan aimsiú + Seol chuig Cógaisíocht + Taispeáin Cód + Teanga + Teanga + Gearmáinis + Araibis + Bulgáiris + Seiceach + Danmhairgis + Béarla + Fraincis + Eabhrais + Iodálach + Ollainnis + Polainnis + Rómáinis + Rúisis + Tuircis + Úcráinis + Réamhshocrú + Tá admhálacha costais dhigiteacha díghníomhaithe agus scriosta. + "Fóram le haghaidh ríomh-oideas" + Ní féidir aon táirge athsholáthair + Is féidir cógais ionaid + druga athsholáthair (Aut idem) + Tá sé de dhualgas ar chógaiseoirí tosaíocht a thabhairt do chógais a dháileadh a bhfuil comhaontú lascaine tugtha i gcrích ag cuideachta árachais sláinte an othair le déantóirí drugaí. Ní bhaineann sé seo ach amháin má eisiann an dochtúir \"Aut idem\" ar an oideas, rud nach bhfuil an cás le d\'oideas. + Tá sé socraithe ag do dhochtúir gur chóir duit an cógas forordaithe a fháil. Níor cheart don chógaslann aon mhalartuithe a dhéanamh bunaithe ar chomhaontú lascaine (“Aut idem”). + Ceadaigh screenshots + Má cheadaíonn tú scáileáin scáileáin, fanfaidh an leathanach deiridh a d’oscail tú le feiceáil sa chúlra nuair a aistríonn tú aipeanna. Seans go mbeidh do shonraí pearsanta le feiceáil mar thoradh air sin. Molaimid mar sin gan scáileáin scáileáin a cheadú. + Ceadaigh + Cealaigh + Athraigh do mhéarchlár go greamáin nó úsáid emojis do do phictiúr próifíle. + Roghnaigh pictiúr próifíle + Conas ar mhaith leat leanúint ar aghaidh? + Roghnaigh grianghraf + Ceamara + Emoji + Cealaigh + Oscail socruithe + Úsáid + Gabháil + Cuir pictiúr próifíle in eagar + Glacadh leis %s nóiméad ó shin + Glactha ar %s + Just a glacadh + Glacadh leis ag %s a chlog + ID Sláinte + Roghnaigh cuideachta árachais + Mura n-oibríonn clárú leis an ID Sláinte mar a bheifí ag súil leis, lean na leideanna inár gcabhair le do thoil. + Cabhrú + Cabhrú + Leideanna maidir le clárú leis an app árachais + Tá do chuideachta árachais freagrach as an ID sláinte. Déan teagmháil leo le do thoil má tá aon cheist agat faoi chlárú. Seo roinnt leideanna triailte agus tástálaithe: + Tabhair faoi deara, ag brath ar d\'árachas, go dteastaíonn aip ar leith. Cuir ceist ar do chuideachta árachais cé acu ceann é seo. + Tosaigh an aip Árachais agus logáil isteach ann uair amháin sula dtosaíonn tú ag logáil isteach ar an aip ríomh-oideas. + D’fhéadfadh fadhbanna a bheith mar thoradh ar aistriú idir logáil isteach le d’ID sláinte agus do chárta sláinte. Logáil isteach go gníomhach amach as do phróifíl ar dtús sula n-athraítear an rogha logáil isteach. + Mura bhfuil d’árachas ar an liosta, is féidir leat logáil isteach le do chárta sláinte agus an UAP comhfhreagrach. + Mura ndéanann aip an árachóra sláinte tú a atreorú ar ais chuig an aip ríomh-oideas, le do thoil déan cinnte an earráid seo a thuairisciú do d’árachóir. + Mura féidir rochtain a fháil ar an aip árachais, b’fhéidir go mbeadh sé ina chuidiú dul chuig socruithe brabhsálaí d’fhón cliste agus ligean do naisc a oscailt. + Má tá do ghuthán cliste ag rith Android 14, d’fhéadfadh sé a bheith ina chuidiú naisc a oscailt sna socruithe a cheadú. + Oscail Socruithe + Ar an drochuair, níor oibrigh sé sin amach + Bain triail eile as níos déanaí. + Teachtaireachtaí earráide agus iad á luchtú. + Roghnaigh emoji nó téacs + Sábháil + Logáil isteach le do thoil chun leanúint ar aghaidh + Ar aghaidh + Glaoigh + Scríobh \nRíomhphost + Go dtí an chógaslann + Cógaisíocht + Inniu + amárach + Bealach\nanseo + Scrios oideas agus admháil costais + Má scriosann tú an t-oideas, scriosfar an admháil speansais ghaolmhar freisin. + Taispeántar na teachtaireachtaí ó gach próifíl le chéile anois + Gnéithe Nua + Gnéithe Nua + Táimid i gcónaí ag obair ar ghnéithe nua. Cuir do thuairim in iúl dúinn agus cuidigh linn aip níos fearr a dhéanamh. + Orduithe do chách + Faoi Orduithe, is féidir leat anois na horduithe do gach próifíl a fheiceáil le chéile agus iad a fheiceáil i bhfad níos éasca + + Gan aisíocaíocht costas + De ghnáth, ní chlúdaíonn árachas sláinte costais an oideas seo. Mar othar, tá tú freagrach mar sin as an méid iomlán a íoc. Ní mór a sheiceáil ar bhonn aonair cibé an n-aisíocfar na costais mar chuid d’árachas forlíontach nó sochair reachtúla. + Ní chlúdóidh d’árachas costas an oideas seo. + + msgstr \"Ní chlúdaíonn d\'árachas an t-oideas %s \" + + + + msgstr \"Ní chlúdaíonn d\'árachas na oidis %s .\" + + ba chúis leis + Dáta + Timpiste + Timpiste ag an obair + Tinneas ceirde + Teachtaireachtaí + Gan teachtaireachtaí + Níl aon teachtaireacht agat fós. + 🎉 Tá d’ordú réidh le bailiú. Taispeáin an cód bailithe seo chun tú féin a shainaithint. + Ar an drochuair, bhí teachtaireacht do chógaslann folamh. Déan teagmháil le do chógaslann le do thoil. + Chuir an chógaslann nasc ar fáil duit. + Teachtaireachtaí + Ordú + Teachtaireachtaí + Gan nasc idirlín + Chun na gléasanna nasctha a thaispeáint, ní mór duit a bheith nasctha trí bhithmhéadracht. + Scriosadh an t-oideas ar %s + Scriosta + Gan oideas + Níl aon oideas agat sa chartlann. + Scriosadh d\'admhálacha costais + Admháil costais + Clárú riachtanach + Logáil isteach le do thoil chun féachaint ar ghléasanna nasctha. + Logáil isteach + Logáil isteach + Ag nascadh le %s Insurance + Cárta sláinte a chuig do smartphone + Gan logáil isteach + Logáilte isteach + athnuachan + Gheobhaidh tú logaí rochtana nuair a bheidh tú logáilte isteach sa tseirbhís oidis. + Clárú ag teastáil + Logáil isteach le do chárta sláinte agus sábháil do shonraí logáil isteach le bithmhéadracht chun na gléasanna nasctha a fheiceáil. + %1$s x Maidin + %1$s x meán lae + %1$s x tráthnóna + %1$s x Oíche + Mura dtugann do dhochtúir a mhalairt duit, is féidir na treoracha úsáide a thuiscint mar seo a leanas: + Thug do dhochtúir an t-eolas seo duit faoi conas do leigheas a ghlacadh. + Níl aon fhaisnéis i d\'oideas maidir le treoracha dáileoga. + Thug do dhochtúir faoi deara gur tugadh treoracha duit maidir le conas do leigheas a ghlacadh nach bhfuil ar an oideas. Mar shampla, d\'fhéadfadh sé seo a bheith ar do phlean cógais. + Gan sonraithe + DJ + treoracha dosage + " %s - %s" + meabhrúchán cógais + meabhrúcháin cógais + cógais ó bhéal + A chuig na meabhrúcháin cógais + maidin + am lóin + tráthnóna + A tráthnóna + Cógas + faisnéis dosage + 1 + %s / %s + 1 dáileog + dosage athrú + Cealaigh + meabhrúchán cógais + plean cógais + Glac an dáileog mar atá forordaithe ag do dhochtúir + Ag %s a chlog tóg %s x + Roghnaigh dátaí + Ar aghaidh + Cealaigh + Ar + as + Rianú + Gan teorainn + Ina n-aonar + An Chéad Lá + Lá Deireanach + Cuir am leis + Uimh meabhrúcháin pill + Is féidir leat meabhrúcháin a shocrú le haghaidh d’oideas + Ar + As + Cuir i gcuimhne dom + Múch optamú ceallraí don aip seo. + Mura ngníomhachtaíonn tú an rogha seo, féadfar meabhrúcháin cógas a thaispeáint go neamhiontaofa. + Cealaigh + Ceadaigh + treoracha dosage + Gan sonraithe + eolas + Cuimhnigh freisin i mód leas iomlán a bhaint ceallraí + dosage athrú + Cainníocht + foirmi + chríochnaigh + gan teorainn + chuig %s + Déan arís %s + Nuair is féidir, úsáidimid an fhaisnéis atá stóráilte san oideas don ríomh. + Cealaigh + Sábháil + Go dtí deireadh an phacáiste + Am + dáileog + Taispeáin cógais + An bhfuil do chógas glactha agat cheana féin? + meabhrúcháin cógais + Gan meabhrúcháin gníomhacha + Níl aon mheabhrúcháin le déanamh agat inniu + Roghnaigh dáta tosaithe + Roghnaigh dáta deiridh + Fáilte + i d’aip ríomh-oideas + Bain triail eile as + Cuir isteach pasfhocal + Cinntigh le do thoil go bhfuil rochtain ag aon duine ar féidir leat an gléas seo a roinnt leo agus a bhfuil d’eolas logáil isteach aige/aici ar do oidis. + Cuir isteach pasfhocal + Cuir isteach an pasfhocal chun an aip a dhíghlasáil. + pasfhocal + Pasfhocal + Cuir isteach pasfhocal chun an aip a dhéanamh slán. + Taispeáin pasfhocal + Déan pasfhocal arís + Dearmad déanta agat ar do phasfhocal? Scrios an aip agus ansin reinstall é. Is féidir leat a fháil amach cén fáth inár %s. + Cuir isteach pasfhocal + Caithfidh an pasfhocal a bheith ocht gcarachtar ar a laghad ar fad + Ní leor neart pasfhocal + Is leor neart pasfhocal + Ní féidir cúltaca gléis + Socraigh cúltaca gléis ar dtús. + Cealaigh + Socruithe + Neart pasfhocal an-mhaith + Slándála app + Cúltaca gléas + Roghnaigh modh amháin ar a laghad chun an aip a chúltaca. + pasfhocal + Athraigh Pasfhocal + Chun scriosadh, ní mór nasc leis an bhFreastalaí Oideas a bhunú + Tá earraí réidh + Tá earraí réidh ó %s + Tá earraí réidh díreach anois + Tá na hearraí réidh le %s nóiméad + Tá earraí réidh ó %s + Díchumasaithe toisc nach bhfuil aon phasfhocal curtha isteach. + Díchumasaithe toisc go bhfuil an focal faire ró-lag. + Díchumasaithe toisc nach ionann na pasfhocail. + Déan iniúchadh + Clár deonaithe orgáin + Clár deonaithe orgán a oscailt? + Déanfar tú a atreorú chuig an gclár deontóirí orgáin. Chun do shonraí deonaithe orgán a fheiceáil agus a athrú, ní mór duit logáil isteach ann. + Oscail + Cealaigh + Feidhm neamhghníomhach sa mhód taispeána + diff --git a/app/features/src/main/res/values-he-rIL/strings.xml b/app/features/src/main/res/values-he-rIL/strings.xml deleted file mode 100644 index 4d0f35c8..00000000 --- a/app/features/src/main/res/values-he-rIL/strings.xml +++ /dev/null @@ -1,968 +0,0 @@ - - - בסדר - לְבַטֵל - חזור - סְבִיב - דִיגִיטָלי. מָהִיר. לבטח. - מזהה משימה - קוד גישה - תנאי שימוש - הגנת מידע - מתכונים - הגישה למצלמה נדחתה - כדי להשתמש בסורק, עליך לאפשר לאפליקציה לגשת למצלמה שלך בהגדרות המערכת. - מקד את המצלמה על קוד מתכון - זה לא קוד מרשם תקף - קוד מרשם זה כבר נסרק - - מתכון %s זוהה - - - זוהו מתכונים %s - - לְבַטֵל - אור מצלמה - לבטל את הסריקה? - בסדר - אל תבטל - בוא נלך - מה אתה צריך: - הזן מספר גישה לכרטיס - הזן את קוד ה-PIN - נסה שוב - החיבור לשרת נכשל. - - יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. - - - יש לך %s ניסיונות נוספים לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. - - אתה יכול למצוא את מספר הגישה בפינה השמאלית העליונה של כרטיס הבריאות שלך. - לְבַטֵל - חפש לפי מפה... - החזק את כרטיס הבריאות כנגד גב המכשיר שלך. - עדיין מחפש … - הזז לאט את הכרטיס בגב המכשיר. - עֵצָה - מקרים של מכשירים עשויים להקשות על החיבור באמצעות NFC. - כרטיס מזוהה - השתדלו לא להזיז את תעודת הבריאות. - נמצא כרטיס בריאות. בבקשה אל תזוז. - החיבור אבד - הצמד שוב את כרטיס הבריאות שלך לגב המכשיר - גרסה: %s - בניית hash: %s - תפריט ניפוי באגים - פתוח עד %s - פָּתוּחַ כָּל הַיוֹם - חוֹתָם - עוֹרֵך - gematik GmbH\n פרידריכשטראסה 136\n 10117 ברלין - מנכ\"ל: ד\"ר. פלוריאן הארטג\'\n בית משפט לרישום: בית המשפט המחוזי ברלין-שרלוטנבורג\n מספר רישום מסחרי: HRB 96351\n מספר זיהוי מע\"מ: DE241843684 - אחראי על התוכן - ד\"ר. פלוריאן הארטג\' - איש קשר - הודעה - אנו שואפים להשתמש בשפה שוויונית בין המינים. אם אתה מבחין בשגיאות, נשמח לשמוע ממך באימייל. - הפלטפורמה המודרנית של גרמניה לרפואה דיגיטלית - כתוב מייל - פתח אתר - ברוך הבא - התחל את הרישום - לבטל נעילה - הירשם - לְבַטֵל - בִּטָחוֹן - משפטי - חוֹתָם - הגנת מידע - תנאי שימוש - פרטים - סמן כמי שנפדה - סמן כלא מומש - צורת מינון - גודל אריזה - מבוטח - שֵׁם מִשׁפָּחָה - כתובת - תאריך לידה - ביטוח בריאות/משלם - סטָטוּס - מספר ביטוח - רושם - שֵׁם מִשׁפָּחָה - רופא מומחה - מספר רופא (LANR) - מוֹסָד - שֵׁם מִשׁפָּחָה - כתובת - מספר צמח - מספר טלפון - כתובת דוא\"ל - תאונת עבודה - יום התאונה - מספר חברה או מעסיק בתאונות - האם תרצה למחוק את המתכון הזה לצמיתות? - לִמְחוֹק - לְבַטֵל - שעות פתיחה - אתר אינטרנט - ניתן לפדות רק היום בתור משלם עצמי - הירשם - הפעל NFC - אנא הפעל את פונקציית ה-NFC של המכשיר שלך כדי להיכנס עם כרטיס הבריאות שלך. - לְהַפְעִיל - נכון - מימוש מרשמים? - האם ברצונך לסמן את המרשמים כממומשים? - לא נפדה - נפדה - נפתח בשעה %s - +49 800 277 377 7 - מוקד טלפוני - פתח סורק למרשמים - הגדרות - הדחק צילומי מסך - מונע הצגת תמונת תצוגה מקדימה בעת החלפת יישומים - האם אתה מאפשר למרשם אלקטרוני לנתח את התנהגות השימוש שלך באופן אנונימי? - מידע טכני - אבטחת נתוני המרשם שלך - אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. - השליחה נכשלה - לא הוגדרה תוכנת דואר אלקטרוני - אין תוצאות - לא הצלחנו למצוא תוצאות עבור מונח חיפוש זה. - רישיונות קוד פתוח - איש קשר - התקשר לתמיכה טלפונית - סקר על האפליקציה - +49 800 277 377 7 - אני רוצה לעזור לשפר את האפליקציה הזו - זה כולל מידע על חומרה ותוכנה על הטלפון שלך, הגדרות של אפליקציית המרשם האלקטרוני והיקף השימוש, אך לעולם לא נתונים אודותיך או בריאותך. - הנתונים יהיו זמינים ל-gematik GmbH רק על ידי הגורם לעיבוד הנתונים ויימחקו לאחר 180 יום לכל המאוחר. אתה יכול לבטל את הניתוח בכל עת בתפריט האפליקציה. - נתונים אלו מאפשרים לנו להבין אילו פונקציות נמצאות בשימוש תכוף ולשפר אותן. אנחנו יכולים גם להעריך כמה זמן צריך לתמוך בטכנולוגיה ישנה יותר ומתי אנחנו יכולים, למשל, להפוך גירסת מערכת הפעלה חדשה יותר לחובה מבלי להשפיע על (יותר מדי) משתמשים. - שפר את האפליקציה - ניתוח אנונימי נשאר מושבת - %s תודה על תמיכתך! - הירשם - אנא הזדהו כדי להוריד מתכונים. - הערה לבתי מרקחת: אנו מקבלים את פרטי ההתקשרות והמידע על בתי מרקחת מאת mein-apothekenportal.de של איגוד בתי המרקחת הגרמני האם גילית שגיאה או שברצונך לתקן נתונים? - למד עוד - בתי מרקחת - למרבה הצער זה לא עבד \uD83D\uDE15 - אנא נסה זאת שוב. - הזן את הסיסמה - נוסף - נְגִישׁוּת - תקריב - מאפשר לך להגדיל את האפליקציה על ידי צביטה לזום. - סיסמה - אבטח את הנתונים שלך עם סיסמה לבחירתך. - סיסמה - להציל - הראה סיסמה - חזור על הסיסמה - המלצות: %s - כתוב מייל - כאשר אתה שולח את ההודעה שלך, המידע הבא על החומרה ומערכת ההפעלה המשמשת מועבר: - מימוש באתר בלבד - אתה עדיין לא יכול לשלוח מרשמים אלקטרוניים לבית המרקחת הזה. - כרגע פתוח - שירות מסנג\'ר - מִשׁלוֹחַ - לְסַנֵן - לְסַנֵן - אין מיקום זמין - מובן - התאמות חוזרות ונשנות של סיסמא - שגיאה 20 10 76631 - תעודת כרטיס הבריאות שלך לא תקפה. אולי פג תוקף הכרטיס שלך? נא לפנות לקופת החולים שלך. - ניסיונות התחברות לא מוצלחים - - זוהו %s ניסיונות התחברות לא מוצלחים. - - - זוהו %s ניסיונות התחברות לא מוצלחים. - - בחר את הגיבוי הטוב ביותר למכשיר - זו יכולה להיות טביעת אצבע, תבנית החלקה או משהו דומה - אסימונים - אסימוני גישה - אסימוני SSO - אין אסימון גישה זמין - אין אסימון SSO זמין - הועתק ללוח - לחץ כדי להעתיק את האסימון ללוח - תקף רק היום - להתיר - אין חיבור לשרת - אנא נסה שוב בעוד מספר דקות - טען שוב - הצג אסימונים - איך תרצו לאבטח את האפליקציה? - הודעה - לא הוגדר גיבוי מכשיר עבור מכשיר זה - אנו ממליצים לך להגן בנוסף על המידע הרפואי שלך באמצעות אבטחת מכשירים כגון קוד או ביומטריה. - אל תציג הודעה זו שוב בעתיד. - חיבור נכשל. לא ניתן היה ליצור חיבור לרשת. - התקשורת עם השרת נכשלה: קוד מצב %s . - התקשורת עם השרת נכשלה: אנא בדוק את חיבור האינטרנט ואת הגדרות השעה/תאריך. - אַזהָרָה - ייתכן שלמכשיר שלך יש אבטחה מופחתת - זה יכול להיגרם, למשל, על ידי התקנים שעברו מניפולציות או כאשר מצב מפתח מופעל. אנו ממליצים לא להשתמש באפליקציה במכשירים שבורים מטעמי אבטחה. - אני מכיר בסיכון המוגבר ועדיין ארצה להמשיך. - מדוע מכשירים עם גישת שורש הם סיכון אבטחה פוטנציאלי? - למד עוד - שם הפרופיל - נא להזין שם לפרופיל החדש. - שם פרופיל - פרופילים - כיצד לזהות כרטיס בריאות התומך ב-NFC - אין אפשרות ליצור קשר דרך האפליקציה הזו - אנא השתמש בערוצים הרגילים כדי ליצור קשר עם חברת הביטוח שלך. - כרטיס בריאות וקוד PIN - PIN בלבד - רישום באפליקציית המרשם האלקטרוני - שדה השם לא יכול להיות ריק. - פרופיל עם השם שהזנת כבר קיים. - פּרוֹפִיל - נבחרו %s - צבע רקע - אפור אביב - טל שמש - זה! האם! וָרוֹד! - עֵץ - ירח כחול ספטמבר - לא מחובר - קשורים ביחד - מחובר לאחרונה ב- %s - מחק פרופיל? - פעולה זו תמחק את כל הנתונים מהפרופיל במכשיר זה. המרשמים שלך ברשת הבריאות יישמרו. - לִמְחוֹק - לְבַטֵל - מחק פרופיל - אתה רוצה למחוק את הפרופיל האחרון. - האפליקציה דורשת לפחות פרופיל אחד. נא להזין שם לפרופיל החדש. - שגיאה 20 10 76831 - לא ניתן היה להגיע אל ספריית כרטיסי הבריאות. בבקשה נסה שוב. - תוכל למצוא מידע מאומת במומחיות על מחלות, קודי ICD ונושאי מניעה וטיפול בפורטל הבריאות הלאומי. - פתח את health.bund.de - שינינו את מדיניות הפרטיות - אפליקציית המרשם האלקטרוני התפתחה. זה הצריך לעדכן את מדיניות הפרטיות שלנו. - פתח את מדיניות הפרטיות - זה השתנה מאז %s : - מה קורה כשפותחים את האפליקציה? - מה קורה אם אני משתמש בפונקציית המצלמה/קורא מתכונים עם המצלמה? - אין מתכונים חדשים זמינים - - המתכון החדש %s - - - %s מתכונים חדשים - - פָּדִי - בגאולה - נפדה - לא ידוע - הצג יומני גישה - מי ניגש למתכונים שלך ומתי? - מפתח גישה לשירות המרשם - יומני גישה - אין יומני גישה - אין עדיין יומני גישה. - המתכון נמצא כעת בתהליך ולא ניתן למחוק אותו - לְקַבֵּל - כנראה שזה לא עבד - אנו מודעים לכך שלקשר עם כרטיס הבריאות יש מלכודות. בעתיד, הרישום אמור להיות אפשרי גם באמצעות אפליקציית ביטוח בריאות מאומתת. \n\n כמו כן, אנו פועלים להבטיח שניתן יהיה לממש מרשמים באופן דיגיטלי ללא הרשמה. \n\n שמתם לב למשהו במהלך התהליך הזה שהייתם רוצים לחלוק איתנו? אנא כתבו לנו, נשמח גם לקבל משוב ביקורתי מאוד. - טיפים לחיבור - שפר את חוזק החיבור - במידת הצורך, הסר את כיסוי המגן. - אם המכשיר רוטט ואז החיבור מתנתק, חפש את המיקום האופטימלי ברדיוס קטן. - העבר את המכשיר לאט מאוד על פני המפה. - הנח את המכשיר ישירות על הכרטיס. - לשם כך, הנח את כרטיס הבריאות על משטח שטוח (למשל שולחן). - שפר את חוזק החיבור - שימו לב למיקום חיישן ה-NFC - גלה היכן נמצא חיישן NFC במכשיר שלך (כאן, למשל, סקירה כללית של מכשירים מ- %s ). - במקרים מסוימים, המיקום של חיישן NFC יכול להיות שונה בתוך סדרת דגמים (כאן, למשל, המידע עבור %s ). - העצה הבאה - נוסף - סגור - לְנַסוֹת - כתוב לנו - חיפוש בית מרקחת ברישיון - לִפְדוֹת - מתכון סרוק - נסרק ב- %s - סומן כמומש ב- %s - איך אתה רוצה להמשיך? - להזמין - זמין בקרוב - הזמן עכשיו לאיסוף או שלח אותו באמצעות שליח או משלוח - שמור להזמנה מאוחרת יותר - שמור מתכונים במכשיר - - המשך עם מתכון %s - - - המשך עם %s מתכונים - - חיבור כרטיס בריאות נכשל - הפרופיל הנוכחי כבר מקושר לכרטיס בריאות אחר (מספר קופת חולים %s ). - כרטיס הבריאות שלך כבר מקושר לפרופיל אחר. עבור לפרופיל %s . - להציל - פרטי התקשרות וכתובת - איש קשר - מספר טלפון - כתובת אימייל (אופציונלי) - כתובת למשלוח - שם פרטי ושם משפחה - מספר רחוב ובית - כתובת נוספת (אופציונלי) - הוראות משלוח (לא חובה) - נדרשים פרטים נוספים ליצירת קשר - בטל שינויים? - להשליך - לצורך החיפוש, ספריית בית המרקחת משתמשת בקואורדינטות גיאוגרפיות שנקבעו בעזרת OpenStreetMap. אנו מודים לפרויקט על העזרה הזו. - © OpenStreetMap ( %s ) - https://www.openstreetmap.org/copyright - הגנת מידע ושימוש - נוסף - קיבלת את ה-PIN עבור כרטיס הבריאות שלך מחברת ביטוח הבריאות שלך באמצעות הליך מאובטח כגון Postident. - לא התקבל PIN - קוד סודי - בדוק את חיבור האינטרנט של המכשיר ואת הגדרות השעה/תאריך. - כדי להיכנס, לחץ על \"בטל נעילה\". - ננעל בחוץ? אנא אמת את האישורים הביומטריים שלך במכשיר זה. - שכחת את הסיסמא? נא למחוק את האפליקציה ולאחר מכן להתקין אותה מחדש. אתה יכול לגלות מדוע ב- %s שלנו. - אזור עזרה - גודל אריזה ויחידה - רכיב פעיל - כמות החומר הפעיל - שם אצווה - Exp - קטגוריה - תַרכִּיב - לְקַבֵּל - בטל - הודעה - עזרו לנו לשפר את האפליקציה הזו - הזן את הסיסמה - הסיסמה חייבת להיות באורך שמונה תווים לפחות - חוזק הסיסמה אינו מספיק - חוזק הסיסמה מספיק - הסיסמה גלויה - הסיסמה אינה גלויה - ביומטריה - סיסמה - ממתין לתשובה - אין מתכונים - אין לך כרגע מרשמים שניתנים למימוש. - לעדכן - יציאה אוטומטית - מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. התחבר מחדש כדי לקבל מתכונים עדכניים. - לְחַבֵּר - קיבלתם תדפיס נייר? - הוסף מתכונים לרשימה שלך על ידי הקשה על כפתור הסריקה בפינה השמאלית העליונה. - סרוק תדפיס נייר - כדי לקבל מרשמים אוטומטית, עליך להיות מחובר. - הירשם - ללא מרשמים מומשים - המרשמים המומשים שלך מוצגים כאן. מטעמי הגנה על נתונים, המתכונים שלך יימחקו משרת המתכונים לאחר 100 ימים. - ללא מרשמים מומשים - המרשמים המומשים שלך מוצגים כאן. הוסף מתכונים באמצעות סריקה כדי להתחיל לממש. - ניהול מכשירים - מכשירים מחוברים - רשום מאז %s (מכשיר זה) - רשום מאז %s - מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. - קוד סודי - הזן PIN (כרטיס בריאות). - נוסף - הירשם - מכשירים מחוברים - הסר מכשיר? - לְבַטֵל - לְהַסִיר - להסיר את המכשיר הזה? - האם ברצונך להסיר %s ? - אם תסיר %s , החיבור לשרת המתכונים ינותק לצמיתות תוך 12 שעות לכל המאוחר. - המכשירים נטענים... - אין מכשירים - אין מכשירים מחוברים לכרטיס בריאות זה. - נסה שוב - או - או :-( - לא ניתן היה לטעון את רשימת ההתקנים. - wwweg… - אין חיבור אינטרנט. - תרופות וחבישות - סמים - חלוקת תרופות מרשם בהתאם לסעיף 4 AMVV - אתה צריך עזרה? - ריכזנו עבורכם כמה טיפים לפתרון הבעיות הנפוצות ביותר. - התחל טיפים לחיבור - לבטל נעילה - כרטיס חסום - ה-PIN הוזן שגוי שלוש פעמים. לכן הכרטיס שלך נחסם לשימוש עם PIN מטעמי אבטחה. - בטל את נעילת הכרטיס - הזן PUK - עם ה-PIN שלך קיבלת מחברת הביטוח שלך קוד PUK בן 8 ספרות. - בחר PIN חדש - אתה יכול לבחור את ה-PIN החדש שלך בעצמך (6 עד 8 ספרות). - זכרתם את ה-PIN שלכם? - אנא רשום את ה-PIN שלך ושמור אותו במקום בטוח. - לְבַטֵל - בסדר - פתיחה לא אפשרית - הגעת למספר המרבי של ביטול נעילה של כרטיס עם PUK זה או שהזנת אותו שוב ושוב באופן שגוי. אנא פנה לחברת הביטוח שלך. - אתה יכול להשתמש ב-PUK אחד עבור עד 10 ביטולי נעילה. - הכרטיס לא נעול - מה אתה צריך: - כרטיס הבריאות שלך - PUK של כרטיס הבריאות שלך - נוסף - כרטיס בריאות - הזמנת PIN או כרטיס - הירשם - איך תרצה לאמת את עצמך? - כרטיס בריאות מאופשר NFC - PIN עבור כרטיס הבריאות - אין לך עדיין כרטיס בריאות ו-PIN התומכים ב-NFC? - הגש בקשה עכשיו - או: היכנס עם %s . - אפליקציית ביטוח הבריאות שלך - "תוכל למצוא את מספר הגישה שלך בפינה השמאלית העליונה של כרטיס הבריאות שלך." - לכרטיס שלי אין מספר גישה - - יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. - - - יש לך %s ניסיונות נוספים לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. - - הנח את כרטיס הבריאות בגב הטלפון - התהליך הבא עשוי להימשך עד 30 שניות. - הנח את הכרטיס %s בגב הטלפון. - באזור הימני העליון - באמצע באזור העליון - באזור השמאלי העליון - באזור האמצעי מימין - אֶמצַע - באזור האמצעי משמאל - באזור הימני התחתון - באמצע באזור התחתון - באזור השמאלי התחתון - עֶזרָה - נשלח לפני %s דקות - נשלח ב- %s - נשלח רק עכשיו - נשלח בזמן %s - לא תקף יותר - הרשמה באפליקציה - בחר ביטוח - לא מצאת את מה שחיפשת? רשימה זו מתרחבת כל הזמן. הרישום בכרטיס בריאות כבר נתמך על ידי כל קופת חולים. - משוב מאפליקציית המרשם האלקטרוני - אנו מצפים למשוב שלך. אנא השתמש במקום הבא ודייק ככל האפשר: - PUK - סגור - חבל… - למרבה הצער, המכשיר שלך אינו עומד בדרישות המינימום לרישום באפליקציית המרשם האלקטרוני. לצורך אימות מאובטח עם כרטיס הבריאות שלך, נדרשים לפחות אנדרואיד 7 ושבב NFC. - למד עוד - לשמור נתוני כניסה? - להציל - אל תחסוך - הודעה - מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. - הגדר אבטחה ביומטרית - לא ניתן לשמור נתוני גישה. לפני כן, הגדר אבטחה ביומטרית (למשל טביעת אצבע) במכשיר שלך. - לְבַטֵל - הגדרות - הודעה - לְקַבֵּל - אבטחת נתוני המרשם שלך - \"אפליקציה זו משתמשת בחיישן הביומטרי המאובטח ביותר שמספק המכשיר שלכם כדי לאבטח את האישורים שלכם באזור מוגן של אחסון המכשיר.\" - האבטחה הביומטרית של נתוני הגישה שלך מאפשרת לך לפתוח את האפליקציה הזו בעתיד, להציג, לאחזר, לממש או למחוק מרשמים ללא כרטיס בריאות והקלדת ה-PIN שלך. - אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. - שלצערי לא עבד - האימות עם אפליקציית ביטוח הבריאות לא הצליח. - פג תוקף ב- %s - המתכון כבר נמחק מהשרת - אנא תקן את הערך שלך או בטל שינויים - נכון - נתוני מבוטח - שֵׁם מִשׁפָּחָה - ביטוח - מספר ביטוח - מספר גישה לכרטיס - הירשם - להתנתק - להציל - שינוי - ערוך תמונת פרופיל - נוסף - השרת לא מגיב - בבקשה נסה שוב מאוחר יותר. - נסה שוב - חפש ביטוח - להתחבר לשרת המתכונים עכשיו? - התחברת בהצלחה - החיבור אבד - להתחבר לשרת המתכונים עכשיו? - אין אסימונים - תקבל אסימון כאשר תיכנס לשירות המרשם.\n - הזמנות - בחר את ה-PIN הרצוי - בטל את נעילת הכרטיס - בחר PIN - חזור על PIN - הערכים שונים זה מזה. - אין פקודות - עדיין אין לך הזמנות. - זֶה עַתָה - בשעה %s - עגלת הקניות מוכנה - המתכון התווסף לעגלת הקניות שלך. נא להיכנס לאתר בית המרקחת להשלמת ההזמנה. - פתיחת עגלת קניות - הצג את קוד האיסוף הזה בבית המרקחת. - קוד איסוף התקבל - אין אפשרות להציג את ההודעה - אנא צור קשר עם בית המרקחת שלך ( %s ). - הצג קישור לעגלת קניות - הצג קוד איסוף - הצג את ההודעה - %s בשעה %s - המתכון נשלח אל %s . תהליך המימוש הדיגיטלי עדיין לא מוכר לבתי מרקחת רבים. אם לא תשמע עד מחר, אנו ממליצים להתקשר לברר כאמצעי זהירות. - סקירת הזמנה - חָדָשׁ - קוּרס - הסדר - ללא תשלום עבור המתקשר. זמני השירות: שני - שישי 8:00 בבוקר - 20:00 למעט בחגים לאומיים - בֵּית מִרקַחַת - בחר את ה-PIN הרצוי - ה-PIN הרצוי נשמר - כרגע פתוח וקרוב אליי - סנן לפי… - להתחיל בחיפוש - משימה ישירה - בתי מרקחת - מספר טלפון (אופציונלי) - חפש לפי שם או כתובת - אין מידע חוקי של בית מרקחת - לא נמצא מידע עדכני על בית מרקחת זה. הערך של בית מרקחת זה יימחק. - בסדר - ספריית בתי המרקחת אינה זמינה - נכון לעכשיו, לא ניתן לגשת למידע עדכני על בית מרקחת זה. אנא בדוק את חיבור האינטרנט שלך. - לְבַטֵל - נסה שוב - שמור סביבה - לא ניתן להיכנס - נראה שמאפייני הכניסה הביומטריים שלך השתנו. אנא היכנס שוב עם כרטיס הבריאות שלך. - לְבַטֵל - הירשם - פרופיל 1 - קרוב אליי - ניתן למימוש מאוחר יותר - ניתן למימוש מ %s - שיפורים במוצר - ניתוח אנונימי - עזרו לנו לשפר את האפליקציה הזו. כל נתוני השימוש נאספים בעילום שם ומשמשים אך ורק לשיפור חווית המשתמש. - אבטחת אפליקציה - הגדרות אישיות - נְגִישׁוּת - שיפורים במוצר - נוסף מתכון - המתכון כבר זמין - אירעה שגיאה במהלך הייבוא - לִמְחוֹק - מתכון סרוק - הכנה להחלפה אפשרית - נשכח PIN - - %s מתכון - - - %s מתכונים - - קראתי ומסכים למדיניות הפרטיות ותנאי השימוש. - הגנת מידע - תנאי שימוש - אנחנו נהיה: - שפר את השימושיות. - זיהוי שגיאות וקריסות. - כמובן, אסוף את כל הנתונים בעילום שם. - אתה יכול לשנות החלטה זו בכל עת בהגדרות המערכת. - לְהַמשִׁיך - לְקַבֵּל - אפליקציה זו משתמשת בשיטה הבטוחה ביותר שמספקת המכשיר שלך. - להציל - בחר - תְרוּפָה - שם מסחרי - כן - לא - מִנוּן - יום הוצאה לאור - מרשם זה ימומש עבורך במסגרת טיפול. - לא מוגדר - תשלום נוסף - תְרוּפָה - הוראות לשימוש - זכאי לפי BVG - הכנה חלופית - שם המתכון - אריזה - הוראות ייצור - תיאור - ניתנו על ידי - הונפק ב: - רכיב פעיל - רשום - לְקַבֵּל - מהי משימה ישירה? - עם הפניה ישירה, מרשם מרופא או בית חולים ממולא ישירות בבית מרקחת. מבוטחים אינם חייבים לבצע כל פעולה ואינם יכולים להתערב בתהליך הפדיון. \n\n הפניות ישירות מופיעות באפליקציית המרשם האלקטרוני כדי להפוך את הטיפול שלך לשקוף יותר עבורך. - עמלת שירות חירום - לפעמים יש צורך בחיפזון. ניתן למלא מרשמים מסוימים ללא תשלום נוסף של דמי שירות חירום, למשל בלילה או בחגים. - תרופות בעלות השתתפות עצמית - פטור מתשלום נוסף - בעלי ביטוח בריאות סטטוטורי חייבים לשלם תשלום נוסף של עד עשרה אירו עבור תרופות מרשם. \n\n גובה התשלום הנוסף תלוי במחיר התרופה שלך. אתה צריך לשלם עבור תרופות שעולות פחות מ-5 אירו בעצמך.\n עבור תרופות יקרות יותר, יש לשלם עשרה אחוזים מהמחיר, אך לפחות 5 אירו ומקסימום 10 אירו. \n\n ילדים וצעירים מתחת לגיל 18 פטורים בדרך כלל מתשלום נוסף. \n\n אם העלויות השנתיות שלך עבור תרופות חורגות ממגבלת הנטל הכספי שלך, תוכל לקבל פטור מהתשלום. שוחח על כך עם קופת החולים שלך. - אתה פטור מתשלום השתתפות עצמית עבור תרופה זו. קופת החולים שלך תכסה את עלות התרופה. - לכמה זמן מרשם זה תקף? - במהלך תקופה זו תוכל לממש את המרשם שלך בכל בית מרקחת בתשלום נוסף של 10 אירו. - הכנה להחלפה אפשרית - עקב דרישות משפטיות מחברת ביטוח הבריאות שלך, ייתכן שתינתן לך חלופה עם אותו חומר פעיל. \n\n תרופות יכולות להיראות ולהיקרא שונות, בעלות מחירים ויצרנים שונים, אך עדיין מכילות את אותו חומר פעיל. לחומר הפעיל עצמו ולמינון יש חשיבות מכרעת להשפעת התרופות בגוף. לעתים קרובות חולים מקבלים בבית המרקחת תרופה שונה מזו שרשם הרופא - בתנאי שהתרופה דומה. ייתכנו סיבות טיפוליות וכלכליות לשינוי. - מתכון סרוק - מרשמים המיובאים מעותק קשיח אינם יכולים להציג מידע אישי או רפואי מסיבות אבטחה. \n\n היכנס לאפליקציה זו באמצעות כרטיס בריאות או אפליקציית ביטוח כדי לראות את כל המידע הכלול במרשם. - מתכון שגוי - מרשם זה הוצא בצורה שגויה. - עמלת שירות חירום - מינון לפי הוראות כתובות - טלפון - אתר אינטרנט - דוֹאַר - מיון לפי מרחק לא אפשרי. - בסדר - הזן PIN נוכחי - הוזן PIN שגוי - ה-PIN הנוכחי של כרטיס הבריאות שלך - כרטיס חסום - בטל את נעילת הכרטיס שלך בהגדרות > ביטול נעילת כרטיס. - מטעמי אבטחה, אנא הזן את ה-PIN הנוכחי שלך. - נשכח PIN - מתכון לא נכון - תְרוּפָה - נראה שמשהו השתבש במהלך יצירת המתכון שלך. לדווח על שגיאה? - להגיש תלונה - לא מחובר - רשום עם - כרטיס בריאות - ביומטריה - לא מחובר - אנו מעוניינים לדעתכם. אנא הקדש חמש דקות למילוי הסקר שלנו. תודה רבה מראש. - הערת אזהרה - בית מרקחת נוסף למועדפים - בית המרקחת הוסר מהמועדפים - בתי המרקחת שלי - חוזק הסיסמא טוב מאוד - פעולת הכתיבה לא הצליחה - לא ניתן היה לשמור את ה-PIN - להגיש תלונה - הקצה PIN - כלל הגישה הופר - אין לך הרשאה לגשת לספריית המפות. - הקצה סיכה משלך - הכרטיס מאובטח ב-PIN מקופת החולים שלך (PIN תחבורה) נא להזין PIN משלך. - הסיסמה לא נמצאה - אין סיסמה מאוחסנת בכרטיס שלך. - נותקת מהמערכת - היכנס שוב כדי לעדכן את המתכונים שלך. - מספר החומר הפעיל - עוצמה ואחדות - מומש לפני %s דקות - מומש ב- %s - נפדה רק עכשיו - מומש בשעה %s - הזמנות - מרשם זה מולא כחלק מטיפול עבורך. - עמלת שירות חירום - לא ניתן למלא מרשם זה בבית מרקחת בלילה ללא תשלום נוסף של דמי שירות חירום. - חפש כאן - הגדרות - שתף מיקום בהגדרות. - קרוב אליי - לחץ והחזק כדי לערוך את השם. - הזן את השם החדש עבור הפרופיל. - כדי לקבל מרשמים באופן דיגיטלי מהמרפאה שלך, עליך להיות מחובר. - מקבלים מרשמים בצורה דיגיטלית? - משוך למטה את המסך כדי לרענן. - אין מתכונים - היכנס כדי לקבל מתכונים באופן אוטומטי או הוסף מתכון חדש באמצעות ⊕ בפינה העליונה. - הירשם - ארכיון מתכונים - אולי אחר כך - הירשם - ערוך תמונת פרופיל - ארכיון מתכונים - הכנס שם - להציל - ההזמנה שלי - נמען: ב - מתכונים - בֵּית מִרקַחַת - לִשְׁלוֹחַ - שינוי - איסוף בבית המרקחת - משלוח באמצעות שליח - משלוח בהזמנה בדואר - %s מתכונים - לא ניתן לפדות - לא ניתן היה לממש מרשם אחד או יותר. - לא נבחר מתכון - כדי לממש מתכונים, יש לבחור לפחות מתכון אחד. - הוסף פרטים ליצירת קשר - שינוי - אין מתכון - אין לך כרגע מרשמים שניתנים למימוש - אוסף - נער שליחויות - מִשׁלוֹחַ - בחר מתכונים - הקש כאן כדי לסרוק מתכונים - לחץ לחיצה ארוכה כדי לערוך שמות - הוסף פרופילים נוספים, למשל עבור הילדים או ההורים שלך - לחץ על התצוגה כדי לדלג על הסבר הכלי שמופיע. - איך לפדות? - כיצד תרצה לקבל את התרופה שלך? - לממש ישירות - מימוש תרופות במקום - להזמין - הזמן או שלח אותו - מוּכָן - קוד איסוף - קודים בודדים - - יש לך מתכון %s . - - - יש לך %s מתכונים. - - לעשות בחירה - כל המתכונים - איזה מתכונים? - נוסף - נוסף - למד עוד - הודעה - אפליקציה זו משתמשת בתוכנה מגוגל כדי לזהות קודים. - למד עוד - מידע על סורק קוד המתכון - אילו נתונים מכיל קוד המתכון? - קוד המתכון מכיל רק מזהה עבור המתכון. המשמעות היא שניתן למצוא את המרשם בשירות המרשם ברשת הבריאות הדיגיטלית. קוד המרשם אינו מכיל מידע אודותיך או אודות התרופה שלך. - אז אף אחד לא יכול לעשות כלום רק עם קוד המתכון? - נכון. יש להוריד את נתוני המרשם משירות המרשם. לשם כך נדרשת התחברות מאובטחת. - מי יכול להירשם לשירות המרשם? - הרשמה לשירות המרשם ברשת הבריאות הדיגיטלית אפשרית למבוטחים, בתי מרקחת, מרפאות ובתי חולים. - מדוע אפליקציית המרשם האלקטרונית משתמשת בתכונות Google? - גוגל מציעה פונקציות שניתן לשלב בקלות באפליקציות ושגוגל מפתחת ומעדכנת ללא הרף. זה מבטיח שהפונקציות פועלות על מכשירים רבים ושונים וניתן להפעיל אותן בבטחה. האפליקציה משתמשת בתכונה כדי לשפר את פונקציונליות המצלמה והסריקה עבור מכשירי אנדרואיד (Google ML Kit). - כיצד פועל שיפור הסריקה עם Google ML Kit? - ערכת Google ML עוזרת לייעל את התמונה שצולמה על ידי מצלמה כך שניתן לקרוא את קודי המתכון גם בתנאי תאורה גרועים או עם דגמי מצלמה ישנים יותר. - האם הנתונים על המרשם או התרופה שלי ישותפו עם Google? - לא. קוד המתכון שנקרא נשמר ישירות באפליקציה. זה לא ישותף עם גוגל. נתוני המרשם אינם נשמרים בקוד, אלא רק ברשת הבריאות הדיגיטלית. משם הם מועברים לאפליקציה. לגוגל אין גישה לרשת הבריאות הדיגיטלית. - אילו נתונים מעבדת Google בעת שימוש ב-ML Kit? - גוגל מקבלת גישה רק למידע טכני על המכשיר בו נעשה שימוש והשימוש הכללי בפונקציה הנוספת (למשל שיעור שגיאות, הגדרות מצלמה) על מנת לתעד זאת באופן סטטיסטי ובכך לשפר את הפונקציה הנוספת. בעת הגישה, Google מתעדת זמנית את כתובת ה-IP של המכשיר שלך. מידע עליך ועל תוכן המתכון אינו מתועד על ידי גוגל. - האם השימוש ב-Google ML Kit הוא התנדבותי? - כן. עם זאת, ML Kit מובנה בסורק קוד המתכון בגרסת האנדרואיד של אפליקציית המרשם האלקטרוני. אם אתה משתמש בסורק קוד המתכון במכשיר אנדרואיד, תמיד נעשה שימוש בפונקציית ML Kit. עם זאת, אתה יכול להימנע משימוש בסורק קוד המתכון. ניתן לטעון את המרשמים שלך לאפליקציה גם אם תיכנס לרשת הבריאות הדיגיטלית עם כרטיס הבריאות האלקטרוני או דרך אפליקציית קופת החולים שלך. - האם אני יכול לראות מי צפה במתכונים שלי? - כן. כל הגישה לנתונים שלך מתועדת במלואה ברשת הבריאות הדיגיטלית. באפליקציית המרשם האלקטרוני תוכל לראות מי ניגש לנתונים שלך. - לאן אוכל לפנות אם יש לי שאלות לגבי האפליקציה או המרשם האלקטרוני? - מידע מפורט ניתן למצוא בהצהרת הגנת המידע. - מספר החבילות שנקבעו - אין מתכונים - בשביל זה אתה צריך מרשמים שניתנים לפדיון. - בחר ביטוח - חפש ביטוח - לְבַטֵל - על מה תרצה להגיש בקשה? - עבור אפליקציה זו אתה צריך כרטיס ואת ה-PIN המשויך. - כיצד תרצה ליצור קשר עם חברת הביטוח שלך? - חברת הביטוח שלך מציעה את אפשרויות ההתקשרות הבאות - חברת הביטוח שלך מציעה את אפשרות ההתקשרות הבאה - סגור - PIN הוזן שגוי. - מספר הגישה הוזן שגוי - PUK הוזן בצורה שגויה. - קבלות עלויות - הצג קבלות עלויות - קבלות עלויות - כדי לקבל קבלות על הוצאות, עליך להיות מחובר לשרת. - לְחַבֵּר - ללא קבלות עלויות - השבת - לְבַטֵל - השבת את הפונקציה - פעולה זו תמחק את כל קבלות ההוצאות מהמכשיר הזה ומהשרת. - קבלת קבלות עלויות - קבלות העלות שלך מאוחסנות גם בשרת המתכונים. - קיבלו - סה\"כ: %s %s - בחר - לְפַצֵל - לִמְחוֹק - לִמְחוֹק - שלח - %s € - מחיר סופי - טיפ: שלח קבלות עלות דרך אפליקציית הביטוח - שלח קבלות עלות בקלות באמצעות האפליקציה של חברת הביטוח שלך. בשלב הבא, בחר את האפליקציה הזו ולחץ על שתף. - תרגול - בֵּית מִרקַחַת - תַאֲרִיך - להראות יותר - מזהה תרופה - הונפק עבור - KVNR: %s - נולד ב: %s - בסדר - איך מגישים מסמכים תומכים? - העבר ישירות לאפליקציה של משרד הביטוח/תגמולים שלך. כדי לעשות זאת, בחר את האפליקציה בעמוד הבא. - אוֹ - שמור את הקובץ וייבא מאוחר יותר לפורטל הביטוח/תגמולים. - מאמר: %s - ספירה: %s - מע\"מ: %s %% - מחיר ברוטו באירו: %s - עמלות נוספות - עמלת שירות חירום - עמלת BTM - דמי מרשם T - עלויות רכש - שירות מסנג\'ר - סך הכל באירו: %s - לִגבּוֹת - באמת למחוק? - הקובץ יימחק מהמכשיר שלך ומהשרת. - לִמְחוֹק - פורסם - מיקוד - מקום - יגאל עבורך - נגאל עבורך - עליך להיות מחובר כדי להשתמש בשירות זה. - כרטיס בריאות - נדרש PIN משויך - ניתן לממש רק מחר כמשלם עצמי - נותרו רק %s ימים למימוש בתור משלם עצמי - \nעדיין ניתן לפדות כתשלום עצמי למשך %s ימים\n - תקף למשך %s ימים בלבד - \nתקף לעוד %s ימים\n - תקף רק מחר - חיובים חלים - לוקח ביטוח - המתכון/ים הועברו בהצלחה. - לא ניתן לעבד את המתכון. בבקשה נסה שוב. ייתכן שתצטרך לבחור בית מרקחת אחר. - לא ניתן לעבד את המתכון. בית המרקחת מדווח על שגיאה לא ידועה. במידת הצורך, נסה בית מרקחת אחר. - המרשם נדחה על ידי בית המרקחת. ייתכן שהמרשם אינו חוקי או שכתובת המשלוח או פרטי הקשר שלך אינם חוקיים. - לא ניתן לממש, אנא בדוק את חיבור האינטרנט שלך. - המתכון הועבר בהצלחה. עם זאת, בית המרקחת מדווח על טעות בעיבוד. נא לפנות לבית המרקחת. - המרשם נדחה על ידי בית המרקחת. המרשם כבר מומש. - המרשם נדחה על ידי בית המרקחת. המתכון נמחק. - לא ניתן היה להעביר את המתכון. אנא בדוק את חיבור האינטרנט שלך ונסה שוב. - לא ניתן היה להעביר מתכון אחד או יותר. - שגיאה בשליחת - נשלח בהצלחה! - טעות בבית המרקחת - טעות בבית המרקחת - פנה לבית מרקחת - מרשם כבר מומש - המתכון נמחק - אין אינטרנט - כדי לקבל יומני גישה, עליך להיות מחובר לשרת. - אתה עדיין יכול למלא את המרשם בבית מרקחת בתוך תקופה זו, אך תצטרך לשלם את כל מחיר הרכישה עבור התרופה בעצמך. לחלופין, אתה יכול לבקש מהמרפאה שלך להנפיק מחדש את המרשם. - מוּכָן - בקש תיקון - בבית המרקחת - באפליקציה - מסור את הקוד הזה בבית המרקחת שלך. - בקשה לתיקון חיוב - תְרוּפָה - אנא הזן לפחות תו אחד. - או: נסה את האפליקציה במצב הדגמה - במצב הדגמה - במצב הדגמה - השתמש במצב הדגמה - מצב הדגמה הופעל - סיים כאן - הפעל מצב הדגמה - לא עכשיו - לחפש - נדרש סמארטפון תומך NFC - קבלת קבלות עלויות באופן דיגיטלי - לאחר הפעלת קבלות העלות, תמצא אותן כאן לאחר מימוש המרשם שלך. - לְהַפְעִיל - קבלת עלות דיגיטלית - ברגע שבית המרקחת הפקיד את קבלה העלות היא תופיע כאן. - קבלת עלות - קבלת קבלות עלויות באופן דיגיטלי - הערה: לא תקבל עוד את קבלות העלות שלך כתדפיס ב-\n בֵּית מִרקַחַת.\n - לְהַפְעִיל - אולי אחר כך - הֶסכֵּם - בהסכמתך, תימנע מהדפסתו בבית המרקחת\n ושלח את קבלות העלות שלך באופן דיגיטלי בעתיד.\n - לְבַטֵל - מוסכם - צא ממצב הדגמה - לסקירה כללית של קבלות העלויות - אל קבלות העלות - כעת אתה מקבל קבלות עלות באופן דיגיטלי - קבלות עלויות כבר תקבלו בדיגיטל - בסדר - נסה שוב - הפונקציה מופעלת - אתה כבר יכול לקבל קבלות עלויות באופן דיגיטלי. - אין אינטרנט - אין חיבור לאינטרנט. - בקשה לא נכונה - הייתה בעיה עם הבקשה. אנחנו עובדים על פתרון.\n - השרת לא מגיב - אנא נסה שוב בעוד מספר דקות. - ההתחברות נכשלה - כרגע לא ניתן לאחזר מידע. אנא\n נסה שוב מאוחר יותר.\n - נִדחֶה - השרת דחה את בקשתך. אנא נסה זאת\n שוב מאוחר יותר.\n - עדכן את האפליקציה - כדי להשתמש בתכונה זו, אנא עדכן את האפליקציה שלך.\n - ההתחברות נכשלה - לשרת הייתה בעיה בכניסה. אנא צור קשר\n שוב.\n - הירשם - העתק כתובת URL - התחבר לאפליקציית ביטוח בריאות חיצונית. - לא ניתן למצוא את אפליקציית ביטוח הבריאות - לא ניתן היה לגשת לאפליקציית ביטוח הבריאות. האימות לא הצליח. - לא אפשרי עבור ביטוח בריאות זה - אנא ספק מספר טלפון ליצירת קשר. - אנא ספק שם פרטי ושם משפחה כדי ליצור איתנו קשר. - אנא ספק רחוב ומספר בית כדי ליצור איתנו קשר. - אנא ספק את המיקוד שלך כדי ליצור איתנו קשר. - אנא ציינו את מקום מגוריכם כדי ליצור איתנו קשר. - מספר הטלפון שהזנת אינו חוקי. (תווים קצרים מדי, ארוכים מדי, לא חוקיים) - אנא הזן כתובת אימייל כדי ליצור איתנו קשר - כתובת האימייל שהוזנה אינה חוקית. - הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) - הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) - הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) - הטקסט שהזנת אינו חוקי. (ארוכים מדי, קצרים מדי, תווים לא חוקיים) - הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) - הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) - נתונים שגויים - נדרש תיקון - נדרשת אפליקציה נוספת - מוּמלָץ - זיהוי בריאות דיגיטלי - לשנות הגדרות? - עמוד 404? - אנא הפעל קישורי פתיחה בהגדרות שלך כדי להמשיך. - פתח את ההגדרות - גרסת אפליקציה מיושנת - הגרסה שלך של האפליקציה מיושנת ואינה נתמכת עוד. כדי להמשיך להשתמש במרשם האלקטרוני, אנא עדכן את האפליקציה. - לעדכן - ה-CAN שלך הוא באורך %s ספרות. - ה-PIN שלך יכול להיות באורך של %s עד %s ספרות. - diff --git a/app/features/src/main/res/values-it/strings.xml b/app/features/src/main/res/values-it/strings.xml index 2f9cbb18..866ade8e 100644 --- a/app/features/src/main/res/values-it/strings.xml +++ b/app/features/src/main/res/values-it/strings.xml @@ -26,7 +26,7 @@ Non annullare Andiamo Quello che ti serve: - Inserisci il numero di accesso della carta + Inserisci il numero di accesso inserire il codice PIN Riprova Impossibile connettersi al server. @@ -55,26 +55,23 @@ impronta editore gematik GmbH\n Friedrichstrasse 136\n 10117 Berlino - Amministratore Delegato: Dott. Florian Hartge\n Tribunale di registrazione: tribunale distrettuale di Berlino-Charlottenburg\n Numero del registro delle imprese: HRB 96351\n Partita IVA: DE241843684 + Direzione: Dott. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nTribunale di registrazione: Tribunale distrettuale di Berlino-Charlottenburg\nNumero del registro delle imprese: HRB 96351\nPartita IVA: DE241843684 Responsabile del contenuto - Dott. Florian Hartge + Dott. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Contatto Avviso Ci sforziamo di utilizzare un linguaggio equo rispetto al genere. Se noti errori, saremo lieti di risponderti via e-mail. La moderna piattaforma tedesca per la medicina digitale Scrivi e-mail Apri il sito web - Benvenuto - Inizia la registrazione Sbloccare Registrati Annulla - Sicurezza Legale impronta protezione dati Termini di utilizzo - Dettagli + Dettagli della ricetta Contrassegna come riscattato Contrassegna come non redento Forma di dosaggio @@ -96,8 +93,6 @@ Numero della pianta Numero di telefono Indirizzo e-mail - Infortunio sul lavoro - Giorno dell\'incidente Numero dell\'azienda o del datore di lavoro Vuoi eliminare definitivamente questa ricetta? Eliminare @@ -120,7 +115,6 @@ Apri lo scanner per le prescrizioni Impostazioni Elimina gli screenshot - Impedisce la visualizzazione di un\'immagine di anteprima quando si cambia app Consenti a E-Prescription di analizzare il tuo comportamento di utilizzo in modo anonimo? Informazioni tecniche Sicurezza dei dati della tua prescrizione @@ -142,23 +136,15 @@ L\'analisi anonima rimane disabilitata %s Grazie per il tuo supporto! Registrati - Per favore identificati per scaricare le ricette. - Nota per le farmacie: i dati di contatto e le informazioni sulle farmacie li otteniamo da mein-apothekenportal.de dell\'Associazione tedesca dei farmacisti Hai scoperto un errore o desideri correggere i dati? + Per favore identificati. + Nota per le farmacie: otteniamo i dettagli di contatto e le informazioni sulle farmacie da mein-apothekenportal.de. Hai scoperto un errore o vuoi correggere i dati? Saperne di più Farmacie Sfortunatamente non ha funzionato \uD83D\uDE15 Per favore riprova. - Inserire la password - Ulteriore Accessibilità - Ingrandisci - Ti consente di ingrandire l\'app pizzicando per ingrandire. - parola d\'ordine - Proteggi i tuoi dati con una password a tua scelta. - parola d\'ordine + Abilita lo zoom Salva - Mostra password - Ripeti la password Raccomandazioni: %s Scrivi e-mail Quando invii il tuo messaggio, vengono trasmesse le seguenti informazioni sull\'hardware e sul sistema operativo utilizzato: @@ -169,7 +155,7 @@ Spedizione filtro Filtro - Nessuna posizione disponibile + Condividi la posizione Inteso Corrispondenze ripetute della password Errore 20 10 76631 @@ -179,8 +165,6 @@ Sono stati rilevati %s tentativi di accesso non riusciti. Sono stati rilevati %s tentativi di accesso non riusciti. - Scegli il miglior backup del dispositivo - Può trattarsi di un\'impronta digitale, di una sequenza di scorrimento o qualcosa di simile Gettoni Token di accesso Token SSO @@ -188,7 +172,7 @@ nessun token SSO disponibile copiato negli appunti Fare clic per copiare il token negli appunti - Valido solo oggi + Riscattabile solo oggi Permettere nessuna connessione al server Riprova tra qualche minuto @@ -254,7 +238,7 @@ %s nuove ricette Riscattabile - Nella redenzione + È in fase di elaborazione Redento Sconosciuto Visualizza i log di accesso @@ -266,7 +250,7 @@ La ricetta è attualmente in elaborazione e non può essere eliminata Accettare Apparentemente non ha funzionato - Siamo consapevoli che il collegamento con la tessera sanitaria ha le sue insidie. In futuro la registrazione dovrebbe essere possibile anche tramite un’app dell’assicurazione sanitaria già autenticata. \n\n Stiamo anche lavorando per garantire che le prescrizioni possano essere riscattate digitalmente senza registrarsi. \n\n Hai notato qualcosa durante questo processo che vorresti condividere con noi? Scriveteci, saremo lieti di ricevere anche feedback molto critici. + Siamo consapevoli che il collegamento con la tessera sanitaria ha le sue insidie. In futuro la registrazione dovrebbe essere possibile anche tramite un’app dell’assicurazione sanitaria già autenticata.\n\nStiamo anche lavorando per garantire che le prescrizioni possano essere riscattate digitalmente senza registrarsi.\n\nHai notato qualcosa durante questo processo che vorresti condividere con noi? Scriveteci, saremo lieti di ricevere anche feedback molto critici. Suggerimenti per la connessione Migliora la forza della connessione Se necessario, rimuovere la copertura protettiva. @@ -289,7 +273,7 @@ Scansionato il %s Contrassegnato come riscattato il %s Come vuoi continuare? - Ordine + Inviare in farmacia Disponibile a breve Prenota ora per il ritiro o ricevilo tramite corriere o spedizione Risparmia per un ordine successivo @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Protezione e utilizzo dei dati Ulteriore - Hai ricevuto il PIN dalla tua cassa malati tramite una procedura sicura come Post Ident. + Dovevi ordinare attivamente il PIN della tua tessera sanitaria presso la tua cassa malati e poi riceverlo tramite un processo sicuro come Postident. Nessun PIN ricevuto Codice PIN Controlla la connessione Internet del tuo dispositivo e le impostazioni di data/ora. - Per accedere, premere \"Sblocca\". Chiuso fuori? Verifica le tue credenziali biometriche su questo dispositivo. - Ha dimenticato la password? Elimina l\'app e reinstallala. Puoi scoprire perché nel nostro %s . zona aiuto Dimensioni della confezione e unità principio attivo @@ -338,10 +320,6 @@ Annullato Avviso Aiutaci a migliorare questa app - Inserire la password - La password deve contenere almeno otto caratteri - La forza della password non è sufficiente - Forza della password sufficiente La password è visibile La password non è visibile biometrica @@ -446,9 +424,6 @@ Inviato proprio ora Inviato alle %s ora Non più valido - Registrati con l\'app - Scegli l\'assicurazione - Non hai trovato quello che cercavi? Questo elenco viene costantemente ampliato. La registrazione con la tessera sanitaria è già supportata da ogni cassa malati. Feedback dall\'app di prescrizione elettronica Non vediamo l\'ora di ricevere il tuo feedback. Si prega di utilizzare il seguente spazio e di essere il più precisi possibile: PUK @@ -468,7 +443,7 @@ Avviso Accettare Sicurezza dei dati della tua prescrizione - \"Questa app utilizza il sensore biometrico più sicuro fornito dal tuo dispositivo per proteggere le tue credenziali in un\'area protetta della memoria del dispositivo.\" + \Questa app utilizza il sensore biometrico più sicuro fornito dal tuo dispositivo per proteggere le tue credenziali in un\'area protetta della memoria del dispositivo.\ La sicurezza biometrica dei tuoi dati di accesso ti permetterà di aprire in futuro questa app, visualizzare, recuperare, riscattare o cancellare prescrizioni senza tessera sanitaria e inserendo il PIN. Assicurati che anche le persone con cui condividi questo dispositivo e le cui caratteristiche biometriche potrebbero essere memorizzate su questo dispositivo abbiano accesso alle tue prescrizioni. che purtroppo non ha funzionato @@ -481,7 +456,7 @@ Cognome Assicurazione Numero di polizza - Numero di accesso alla carta + Numero di accesso Registrati Disconnettersi Salva @@ -497,8 +472,7 @@ Collegamento perso Connettersi al server delle ricette adesso? Nessun gettone - Riceverai un token quando avrai effettuato l\'accesso al servizio di prescrizione.\n - Ordini + Riceverai un token quando avrai effettuato l\'accesso al servizio di prescrizione. Selezionare il PIN desiderato Sblocca la carta Seleziona PIN @@ -515,26 +489,26 @@ Codice di ritiro ricevuto Il messaggio non può essere visualizzato Contatta la tua farmacia ( %s ). - Mostra il collegamento al carrello + Collegamento farmacia Mostra il codice di ritiro Mostra il messaggio %s alle %s in punto - Ricetta inviata a %s . Il processo di riscatto digitale è ancora sconosciuto a molte farmacie. Se non ricevete risposta entro domani, vi consigliamo di chiamare per chiedere informazioni in via precauzionale. + Ricetta inviata a %s . Alcune farmacie non dispongono ancora di un\'opzione di risposta digitale. Se non si riceve risposta entro domani, chiamare per precauzione. Panoramica dell\'ordine Nuovo Corso L\'ordine - Gratuito per il chiamante. Orari di servizio: lun - ven 8:00 - 20:00 esclusi festivi nazionali + Gratuito per il chiamante. Orari di servizio: lun - ven 8:00 - 20:00 esclusi i giorni festivi federali Farmacia Selezionare il PIN desiderato PIN desiderato salvato Attualmente aperto e vicino a me Filtra per… - Inizia la ricerca + Cercare Assegnazione diretta Farmacie Numero di telefono (facoltativo) - Cerca per nome o indirizzo + Cercare Nessuna informazione valida sulla farmacia Non è stata trovata alcuna informazione attuale su questa farmacia. La voce relativa a questa farmacia verrà eliminata. OK @@ -554,16 +528,15 @@ Miglioramenti del prodotto Analisi anonima Aiutaci a migliorare questa app. Tutti i dati di utilizzo vengono raccolti in forma anonima e vengono utilizzati esclusivamente per migliorare l\'esperienza dell\'utente. - Sicurezza dell\'app impostazioni personali Accessibilità Miglioramenti del prodotto Aggiunta ricetta Ricetta già disponibile Si è verificato un errore durante l\'importazione + Numero di accesso Eliminare Ricetta scannerizzata - Possibile preparazione sostitutiva PIN dimenticato %s Ricetta @@ -579,7 +552,7 @@ Puoi modificare questa decisione in qualsiasi momento nelle impostazioni di sistema. Continua Accettare - Questa app utilizza il metodo più sicuro fornito dal tuo dispositivo. + L\'app utilizza il metodo più sicuro che hai impostato sul tuo dispositivo. Salva Scegliere farmaco @@ -607,15 +580,13 @@ Che cos\'è un incarico diretto? Con la prescrizione diretta, una prescrizione da uno studio o da un ospedale viene compilata direttamente in farmacia. Gli assicurati non devono intraprendere alcuna azione e non possono intervenire nel processo di riscatto. \n\n I referral diretti sono elencati nell\'app di prescrizione elettronica per rendere il trattamento più trasparente per te. Tassa per il servizio di emergenza - A volte la fretta è necessaria. Alcune prescrizioni possono essere soddisfatte senza il pagamento aggiuntivo di una tariffa per il servizio di emergenza, ad esempio di notte o nei giorni festivi. + Se la prescrizione viene compilata tra le 20:00 e le 6:00 o la domenica e i giorni festivi, potrebbe essere addebitato un costo aggiuntivo di 2,50 euro. Farmaci soggetti a ticket Esente da pagamento aggiuntivo - Quelli con l\'assicurazione sanitaria pubblica devono pagare un pagamento aggiuntivo fino a dieci euro per i farmaci soggetti a prescrizione. \n\n L\'importo del pagamento aggiuntivo dipende dal prezzo del farmaco. I farmaci che costano meno di 5 euro li devi pagare tu.\n Per i medicinali più costosi bisogna pagare il 10% del prezzo, ma almeno 5 euro e un massimo di 10 euro. \n\n I bambini e i giovani sotto i 18 anni sono generalmente esenti dal pagamento aggiuntivo. \n\n Se i costi annuali per i farmaci superano il limite dell’onere finanziario, puoi essere esentato dalla partecipazione ai costi. Parlane con la tua compagnia di assicurazione sanitaria. + Le persone con l\'assicurazione sanitaria pubblica pagano solitamente un massimo di 10 euro per i farmaci prescritti. Potrebbero essere applicate tariffe più elevate se viene richiesto un farmaco di un produttore specifico che non è coperto da un contratto di sconto dell\'assicurazione sanitaria (\"farmaco richiesto\"). \n\n I bambini e i giovani sotto i 18 anni sono esenti da pagamenti aggiuntivi. \n\n Se le prescrizioni vengono riscattate dopo 28 giorni dalla loro emissione, i costi devono essere interamente a carico. \n\n Se durante l’anno spendete molti medicinali potete richiedere alla vostra cassa malattia un’esenzione dalla partecipazione ai costi Sei esente dal pagamento di un ticket per questo farmaco. La vostra compagnia di assicurazione sanitaria coprirà il costo del farmaco. Per quanto tempo è valida questa prescrizione? Durante questo periodo puoi riscattare la tua ricetta in qualsiasi farmacia con un pagamento aggiuntivo massimo di 10€. - Possibile preparazione sostitutiva - A causa dei requisiti legali della vostra compagnia di assicurazione sanitaria, potrebbe esservi offerta un\'alternativa con lo stesso principio attivo. \n\n I medicinali possono apparire ed essere chiamati in modo diverso, hanno prezzi e produttori diversi, ma contengono comunque lo stesso principio attivo. Il principio attivo stesso e il dosaggio sono cruciali per l’effetto dei medicinali nel corpo. Spesso i pazienti ricevono in farmacia un farmaco diverso da quello prescritto dal medico, a condizione che il farmaco sia comparabile. Potrebbero esserci ragioni terapeutiche ed economiche per il cambiamento. Ricetta scannerizzata Le prescrizioni importate da una copia cartacea non possono visualizzare informazioni personali o mediche per motivi di sicurezza. \n\n Accedi a questa app con tessera sanitaria o app Assicurazione per visualizzare tutte le informazioni contenute nella ricetta. Ricetta errata @@ -625,7 +596,7 @@ telefono sito web Posta - L\'ordinamento per distanza non è possibile. + Condividi la tua posizione per trovare le farmacie vicino a te. OK Inserisci il PIN attuale È stato inserito un PIN errato @@ -643,12 +614,10 @@ Tessera sanitaria biometrica Accesso non effettuato - Siamo interessati alla tua opinione. Ti preghiamo di dedicare cinque minuti per completare il nostro sondaggio. Grazie mille in anticipo. Avviso di avvertenza Farmacia aggiunta ai preferiti Farmacia rimossa dai preferiti Le mie farmacie - Forza della password molto buona Operazione di scrittura non riuscita Impossibile salvare il PIN Rapporto @@ -667,7 +636,6 @@ Riscattato il %s Redento proprio adesso Riscattato alle %s in punto - Ordini Questa prescrizione è stata compilata come parte di un trattamento per te. Tassa per il servizio di emergenza Questa prescrizione non può essere compilata di notte in farmacia senza il pagamento aggiuntivo di una tassa per il servizio di emergenza. @@ -681,7 +649,7 @@ Ricevere prescrizioni in formato digitale? Abbassa lo schermo per aggiornare. Nessuna ricetta - Accedi per ricevere le ricette automaticamente o aggiungi una nuova ricetta utilizzando ⊕ nell\'angolo in alto. + Iscriviti per ricevere le ricette automaticamente. Registrati Archivio ricette Forse più tardi @@ -718,9 +686,9 @@ Fare clic sul display per ignorare la descrizione comando visualizzata. Come riscattare? Come vorresti ricevere i tuoi farmaci? - Riscatta direttamente - Riscattare i farmaci in loco - Ordine + Mostra codice + Fatelo scansionare in farmacia + Inviare in farmacia Prenota o ricevilo Pronto Codice di raccolta @@ -786,7 +754,7 @@ Ciò eliminerà tutte le ricevute di spesa da questo dispositivo e dal server. Ricevere ricevute di spesa Anche le vostre ricevute di spesa vengono salvate sul server delle ricette. - Ricevuto + Attivare Totale: %s %s Scegliere Diviso @@ -835,17 +803,17 @@ È richiesto il PIN associato Può essere riscattato solo domani come autopagante Rimangono solo %s giorni per riscattare come pagatore autonomo - \nAncora riscattabile come pagatore autonomo per %s giorni\n - Valido solo per %s giorni - \nValido per %s giorni rimasti\n - Valido solo domani + Ancora riscattabile come pagatore autonomo per %s giorni + Rimangono solo %s giorni per riscattare + Ancora riscattabile per %s giorni + Riscattabile solo domani Si applicano costi Prende l\'assicurazione Le ricette sono state trasferite con successo. La ricetta non può essere elaborata. Per favore riprova. Potrebbe essere necessario scegliere una farmacia diversa. La ricetta non può essere elaborata. La farmacia segnala un errore sconosciuto. Se necessario, prova un\'altra farmacia. La ricetta è stata respinta dalla farmacia. La prescrizione potrebbe non essere valida oppure il tuo indirizzo di consegna o le informazioni di contatto potrebbero non essere validi. - Impossibile riscattare, controlla la tua connessione Internet. + Riprovare ed eventualmente scegliere una farmacia diversa. Se l\'errore persiste, contattare l\'assistenza. La ricetta è stata trasferita con successo. La farmacia segnala però un errore di elaborazione. Si prega di contattare la farmacia. La ricetta è stata respinta dalla farmacia. La prescrizione è già stata riscattata. La ricetta è stata respinta dalla farmacia. La ricetta è stata cancellata. @@ -878,7 +846,6 @@ Attiva la modalità demo Non adesso Ricerca - È necessario uno smartphone abilitato NFC Ricevi le ricevute dei costi in formato digitale Una volta attivate le ricevute di spesa, le troverete qui dopo aver riscattato la vostra ricetta. Attivare @@ -886,11 +853,11 @@ Non appena la farmacia avrà depositato la ricevuta di spesa, questa apparirà qui. Ricevuta di spesa Ricevi le ricevute dei costi in formato digitale - Nota: non riceverai più le ricevute dei costi come stampa nel\n Farmacia.\n + Nota: in farmacia non riceverete più i giustificativi dei costi sotto forma di stampa. Attivare Forse più tardi accordo - Con il tuo consenso ti asterrai dal stamparlo in farmacia\n e in futuro invierà le ricevute dei costi in formato digitale.\n + Con il tuo consenso eviterai di stamparli in farmacia e in futuro invierai le tue ricevute di spesa in formato digitale. Annulla Concordato Uscire dalla modalità demo @@ -905,17 +872,17 @@ Senza internet Non c\'è connessione a Internet. Richiesta errata - Si è verificato un problema con la richiesta. Lavoriamo su una soluzione.\n + Si è verificato un problema con la richiesta. Lavoriamo su una soluzione. il server non risponde Riprova tra qualche minuto. Connessione fallita - Al momento non è possibile recuperare alcuna informazione. Per favore\n riprovare più tardi.\n + Al momento non è possibile recuperare alcuna informazione. Per favore riprova più tardi. Respinto - Il server ha rifiutato la tua richiesta. Per favore, provalo\n di nuovo più tardi.\n + Il server ha rifiutato la tua richiesta. Per favore riprova più tardi. Aggiorna l\'app - Per utilizzare questa funzione, aggiorna la tua app.\n + Per utilizzare questa funzione, aggiorna la tua app. Accesso non riuscito - Il server ha avuto un problema durante l\'accesso. Si prega di mettersi in contatto\n Ancora.\n + Il server ha avuto un problema durante l\'accesso. Per favore accedi di nuovo. Registrati Copia l\'URL Connettiti all\'app dell\'assicurazione sanitaria esterna. @@ -941,12 +908,285 @@ Consigliato Tessera sanitaria digitale Modificare le impostazioni? - 404 pagine? Abilita i link di apertura nelle impostazioni per continuare. Apri Impostazioni Versione dell\'app obsoleta La tua versione dell\'app è obsoleta e non è più supportata. Per continuare a utilizzare la prescrizione elettronica, aggiorna l\'app. Aggiornare - Il tuo CAN è lungo %s cifre. + Il tuo numero di accesso è lungo %s cifre. Il PIN può contenere da %s a %s cifre. + Raccolta + Corriere + Spedizione + È necessario uno smartphone abilitato NFC + Non siamo riusciti a elaborare la richiesta di eliminazione della ricetta. Stiamo lavorando a una soluzione ( %s ). + Qualcosa è andato storto durante l\'eliminazione della mia prescrizione, ho ricevuto il codice di errore %s . Avvisami quando sarà nuovamente possibile la cancellazione. + Ulteriore + Richiesta errata + Rapporto + Elimina localmente + Senza internet + Impossibile eliminare la ricetta. Per favore controlla la tua connessione Internet e prova di nuovo. + Riprova + OK + Effettuare nuovamente l\'accesso + Devi essere loggato per eliminare la ricetta (401). + Registrati + Annulla + Cancellazione impossibile + Non è consentito eliminare la prescrizione in questo momento perché è in farmacia per essere compilata o è un incarico diretto in sospeso (403). + Troppe richieste + Hai provato a eliminare la ricetta troppe volte. Riprova più tardi (429). + Elimina ricetta + Ops... + "Si è verificato un errore imprevisto del server. Riprova più tardi (500)." + La sicurezza dell\'app non è possibile. Configura innanzitutto la sicurezza biometrica (ad esempio l\'impronta digitale) sul tuo dispositivo. + Il backup biometrico non è possibile per questo dispositivo poiché non è supportato dal tuo dispositivo. + Pericolo! Il tuo telefono non è ben protetto. + I tuoi dati di accesso vengono salvati sul tuo telefono. Per proteggere l\'accesso, viene utilizzato il metodo di accesso preferito. Questo metodo di accesso, ad esempio la sequenza di scorrimento, non è molto sicuro. L\'utilizzo della funzione \"Salva dati di accesso\" è a tuo rischio e pericolo. + Se utilizzi ancora la funzione \"Salva dati di accesso\", in futuro potrai visualizzare, accedere, riscuotere o cancellare le prescrizioni con l\'app e-prescription senza tessera sanitaria e senza inserimento del PIN. + Posizione non trovata + Inviare in farmacia + Mostra codice + Lingua + Lingua + Tedesco + Arabo + Bulgaro + Ceco + Danese + Inglese + Francese + Ebraico + Italiano + Olandese + Polacco + Rumeno + Russo + Turco + Ucraino + Predefinito + Le ricevute di spesa digitali sono state disattivate e cancellate. + “Forum sulla prescrizione elettronica” + Non è possibile alcuna preparazione sostitutiva + Possibile preparazione sostitutiva + Preparazione sostitutiva (Aut idem) + I farmacisti sono tenuti a dare priorità alla dispensazione dei medicinali per i quali la cassa malati del paziente ha stipulato un accordo di sconto con i produttori dei medicinali. Ciò non si applica solo se il medico esclude la dicitura “Aut idem” nella prescrizione, cosa che non è il caso della tua prescrizione. + Il medico ha stabilito che dovresti ricevere il farmaco prescritto. La farmacia non dovrebbe effettuare uno scambio sulla base di un accordo di sconto (“Aut idem”). + Consenti screenshot + Se consenti gli screenshot, l\'ultima pagina aperta rimarrà visibile in background quando cambi app. Se necessario i tuoi dati personali potranno essere visibili. Consigliamo pertanto di non consentire gli screenshot. + Permettere + Annulla + Passa alla tastiera con gli adesivi o usa gli emoji per l\'immagine del tuo profilo. + Scegli l\'immagine del profilo + Come vuoi continuare? + Seleziona foto + telecamera + Emoji + Annulla + Apri Impostazioni + Utilizzo + Fai una foto + Modifica la foto profilo + Presupposto %s minuti fa + Accettato il %s + Appena accettato + Accettato alle %s in punto + IDsalute + Scegli l\'assicurazione + Se la registrazione con l\'ID sanitario non va come previsto, segui i suggerimenti del nostro centro assistenza. + Aiuto + Aiuto + Consigli per la registrazione all\'app Assicurazioni + La vostra compagnia assicurativa è responsabile della tessera sanitaria. Si prega di contattarli se avete domande sulla registrazione. Ecco alcuni suggerimenti provati e testati: + Tieni presente che, a seconda della tua assicurazione, potrebbe essere necessaria un\'app separata. Ti invitiamo a verificare con la tua compagnia assicurativa per scoprire di cosa si tratta. + Avvia l\'app del registratore di cassa ed effettua il login una volta prima di iniziare a registrarti nell\'app di prescrizione elettronica. + Il passaggio dalla registrazione con HealthID a quella con tessera sanitaria può comportare problemi. Pertanto, prima di modificare l\'opzione di accesso, esci attivamente dal tuo profilo. + Se la tua assicurazione non è presente nell\'elenco puoi in alternativa registrarti con la tua tessera sanitaria e il PIN associato. + Se l\'app del registratore di cassa non ti reindirizza all\'app di prescrizione elettronica, assicurati di segnalare questo errore alla tua compagnia assicurativa. + Se non è possibile accedere all\'app dell\'assicurazione, può essere utile accedere alle impostazioni del browser del tuo smartphone e consentire l\'apertura dei collegamenti. + Se il tuo smartphone utilizza Android 14, potrebbe essere utile consentire l\'apertura dei collegamenti nelle impostazioni. + Apri Impostazioni + che purtroppo non ha funzionato + Per favore riprova più tardi. + Messaggi di errore durante il caricamento. + Scegli un emoji o un testo + Salva + Per favore accedi per continuare + Ulteriore + Chiamata + Scrivere\nPosta + Alla farmacia + Farmacia + Oggi + Domani + Qui\nitinerario + Eliminare la prescrizione e la ricevuta dei costi + Se si elimina la ricetta, viene eliminata anche la ricevuta costi associata. + I messaggi di tutti i profili vengono ora visualizzati insieme + Nuove caratteristiche + Nuove caratteristiche + Lavoriamo costantemente su nuove funzionalità. Condividi la tua opinione con noi e aiutaci a creare un\'app migliore. + Ordini per tutti + Sotto Ordini ora puoi vedere gli ordini per tutti i profili insieme e visualizzarli molto più facilmente + + Nessun rimborso spese + Di norma l’assicurazione sanitaria non copre i costi di questa prescrizione. Come paziente sei quindi responsabile del pagamento completo. Occorre verificare individualmente se i costi verranno rimborsati nell\'ambito di un\'assicurazione complementare o di prestazioni previste dalla legge. + La tua assicurazione non coprirà alcun costo per questa prescrizione. + + "La tua assicurazione non coprirà la prescrizione %s " + "La tua assicurazione non coprirà le prescrizioni %s ." + + Causato + Data + Incidente + Infortunio sul lavoro + Malattia industriale + Notizia + Nessuna notizia + Non hai ancora nessun messaggio. + 🎉 Il tuo ordine è pronto per il ritiro. Mostra questo codice di ritiro per identificarti. + Sfortunatamente, il messaggio della tua farmacia era vuoto. Si prega di contattare la vostra farmacia. + La farmacia ti ha fornito un link. + Notizia + Ordine + Messaggi + Nessuna connessione Internet + Per visualizzare i dispositivi connessi è necessario essere connessi alla biometria. + La prescrizione è stata eliminata il %s + Eliminato + Nessuna ricetta + Non hai ricette in archivio. + Le tue ricevute di spesa sono state cancellate + Ricevuta di spesa + È richiesta la registrazione + Effettua il login per visualizzare i dispositivi collegati. + Registrati + Registrati + Connettiti a %s Assicurazione + Tessera sanitaria sullo smartphone + Non effettuato l\'accesso + Registrato + aggiornare + Riceverai i log di accesso se hai effettuato l\'accesso al servizio di prescrizione. + È richiesta la registrazione + Effettua il login con la tua tessera sanitaria e salva i dati di accesso con dati biometrici per visualizzare i dispositivi connessi. + %1$s x Mattina + %1$s x pranzo + %1$s x Serata + %1$s x Di notte + A meno che il medico non le abbia dato istruzioni diverse, le istruzioni per l’uso possono essere intese come segue: + Il tuo medico ti ha dato queste informazioni sull\'assunzione del farmaco. + Nella prescrizione non ci sono informazioni su come assumere il farmaco. + Il tuo medico ha notato che ti sono state fornite istruzioni su come assumere questo farmaco che non è sulla prescrizione. Questo potrebbe essere incluso nel tuo piano terapeutico, ad esempio. + Non specificato + DJ + Istruzioni per l\'uso + " %s - %s" + Promemoria entrate + Prendi promemoria + Farmaci orali + Informazioni sui promemoria di assunzione + Mattina + Mezzogiorno + Pomeriggio + Di sera + farmaco + Informazioni sul dosaggio + 1 + %s / %s + 1 dose + Modificare il dosaggio + Annulla + Promemoria farmaci + Programma dei farmaci + Assumere la posologia secondo prescrizione del medico + Alle %s prendi %s x + Seleziona le date + Ulteriore + Cancellare + UN + Fuori + Monitoraggio + Illimitato + Individualmente + Primo giorno + Ultimo giorno + Aggiungi tempo + Nessun promemoria di assunzione + Puoi impostare promemoria per le tue prescrizioni + Acceso + Spento + Ricordati di me + Disattiva l\'ottimizzazione della batteria per questa app. + Se non attivi questa opzione, i promemoria dei farmaci potrebbero apparire inaffidabili. + Annulla + Permettere + Istruzioni per l\'uso + Non specificato + informazioni + Ricorda anche in modalità di ottimizzazione della batteria + Modificare il dosaggio + Folla + modulo + finito + illimitato + a %s + Ripetere %s + Ove possibile, per il calcolo utilizziamo le informazioni memorizzate nella prescrizione. + Annulla + Salva + Fino alla fine del pacchetto + Tempo + dose + Visualizza i farmaci + Hai già preso le medicine? + Prendi promemoria + Nessun ricordo attivo + Non hai promemoria da prendere oggi + Seleziona la data di inizio + Seleziona la data di fine + Benvenuto + nella tua app di prescrizione elettronica + Riprova + Inserisci la password + Assicurati che chiunque con cui condividi questo dispositivo e che conosce le tue informazioni di accesso abbia accesso anche alle tue ricette. + Inserisci la password + Inserisci la password per sbloccare l\'app. + password + Password + Inserisci una password per proteggere l\'app. + Mostra password + Ripeti la password + Hai dimenticato la password? Elimina l\'app e reinstallala. Puoi scoprire perché nel nostro %s. + Inserisci la password + La password deve contenere almeno otto caratteri + La forza della password non è sufficiente + Forza della password sufficiente + Backup del dispositivo non possibile + Configura prima un backup del dispositivo. + Cancellare + Impostazioni + Forza della password molto buona + Sicurezza dell\'app + Backup del dispositivo + Scegli almeno un metodo per eseguire il backup dell\'app. + password + Cambiare la password + Per eliminare è necessario stabilire una connessione al Prescription Server + Le merci sono pronte + Le merci sono pronte dal %s + La merce è pronta proprio adesso + La merce è pronta da %s minuti + Le merci sono pronte dal %s + Disabilitato perché non è stata inserita alcuna password. + Disabilitato perché la password è troppo debole. + Disabilitato perché le password non corrispondono. + Esplorare + Registro delle donazioni di organi + Registro delle donazioni di organi aperto? + Verrai reindirizzato al registro dei donatori di organi. Per visualizzare e modificare i dati sulla donazione degli organi, è necessario effettuare il login lì. + Aprire + Cancellare + Funzione non attiva in modalità demo diff --git a/app/features/src/main/res/values-iw/strings.xml b/app/features/src/main/res/values-iw/strings.xml new file mode 100644 index 00000000..a3acc445 --- /dev/null +++ b/app/features/src/main/res/values-iw/strings.xml @@ -0,0 +1,1210 @@ + + + בסדר + לְבַטֵל + חזור + סְבִיב + דִיגִיטָלי. מָהִיר. לבטח. + מזהה משימה + קוד גישה + תנאי שימוש + הגנת מידע + מתכונים + הגישה למצלמה נדחתה + כדי להשתמש בסורק, עליך לאפשר לאפליקציה לגשת למצלמה שלך בהגדרות המערכת. + מקד את המצלמה על קוד מתכון + זה לא קוד מרשם תקף + קוד מרשם זה כבר נסרק + + מתכון %s זוהה + + + זוהו מתכונים %s + + לְבַטֵל + אור מצלמה + לבטל את הסריקה? + בסדר + אל תבטל + בוא נלך + מה אתה צריך: + הזן מספר גישה + הזן את קוד ה-PIN + נסה שוב + החיבור לשרת נכשל. + + יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. + + + יש לך %s ניסיונות נוספים לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. + + אתה יכול למצוא את מספר הגישה בפינה השמאלית העליונה של כרטיס הבריאות שלך. + לְבַטֵל + חפש לפי מפה... + החזק את כרטיס הבריאות כנגד גב המכשיר שלך. + עדיין מחפש … + הזז לאט את הכרטיס בגב המכשיר. + עֵצָה + מקרים של מכשירים עשויים להקשות על החיבור באמצעות NFC. + כרטיס מזוהה + השתדלו לא להזיז את תעודת הבריאות. + נמצא כרטיס בריאות. בבקשה אל תזוז. + החיבור אבד + הצמד שוב את כרטיס הבריאות שלך לגב המכשיר + גרסה: %s + בניית hash: %s + תפריט ניפוי באגים + פתוח עד %s + פָּתוּחַ כָּל הַיוֹם + חוֹתָם + עוֹרֵך + gematik GmbH\n פרידריכשטראסה 136\n 10117 ברלין + הנהלה: דר. פלוריאן פיהרמן, ברניה אדג\'י, ד\"ר. Florian Hartge\nבית משפט לרישום: בית המשפט המחוזי של ברלין-שרלוטנבורג\nמספר רישום מסחרי: HRB 96351\nמספר זיהוי מע\"מ: DE241843684 + אחראי על התוכן + דר. פלוריאן פיהרמן, ברניה אדג\'י, ד\"ר. פלוריאן הארטג\' + איש קשר + הודעה + אנו שואפים להשתמש בשפה שוויונית בין המינים. אם אתה מבחין בשגיאות, נשמח לשמוע ממך באימייל. + הפלטפורמה המודרנית של גרמניה לרפואה דיגיטלית + כתוב מייל + פתח אתר + לבטל נעילה + הירשם + לְבַטֵל + משפטי + חוֹתָם + הגנת מידע + תנאי שימוש + פרטי מתכון + סמן כמי שנפדה + סמן כלא מומש + צורת מינון + גודל אריזה + מבוטח + שֵׁם מִשׁפָּחָה + כתובת + תאריך לידה + ביטוח בריאות/משלם + סטָטוּס + מספר ביטוח + רושם + שֵׁם מִשׁפָּחָה + רופא מומחה + מספר רופא (LANR) + מוֹסָד + שֵׁם מִשׁפָּחָה + כתובת + מספר צמח + מספר טלפון + כתובת דוא\"ל + מספר חברה או מעסיק בתאונות + האם תרצה למחוק את המתכון הזה לצמיתות? + לִמְחוֹק + לְבַטֵל + שעות פתיחה + אתר אינטרנט + ניתן לפדות רק היום בתור משלם עצמי + הירשם + הפעל NFC + אנא הפעל את פונקציית ה-NFC של המכשיר שלך כדי להיכנס עם כרטיס הבריאות שלך. + לְהַפְעִיל + נכון + מימוש מרשמים? + האם ברצונך לסמן את המרשמים כממומשים? + לא נפדה + נפדה + נפתח בשעה %s + +49 800 277 377 7 + מוקד טלפוני + פתח סורק למרשמים + הגדרות + הדחק צילומי מסך + האם אתה מאפשר למרשם אלקטרוני לנתח את התנהגות השימוש שלך באופן אנונימי? + מידע טכני + אבטחת נתוני המרשם שלך + אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. + השליחה נכשלה + לא הוגדרה תוכנת דואר אלקטרוני + אין תוצאות + לא הצלחנו למצוא תוצאות עבור מונח חיפוש זה. + רישיונות קוד פתוח + איש קשר + התקשר לתמיכה טלפונית + סקר על האפליקציה + +49 800 277 377 7 + אני רוצה לעזור לשפר את האפליקציה הזו + זה כולל מידע על חומרה ותוכנה על הטלפון שלך, הגדרות של אפליקציית המרשם האלקטרוני והיקף השימוש, אך לעולם לא נתונים אודותיך או בריאותך. + הנתונים יהיו זמינים ל-gematik GmbH רק על ידי הגורם לעיבוד הנתונים ויימחקו לאחר 180 יום לכל המאוחר. אתה יכול לבטל את הניתוח בכל עת בתפריט האפליקציה. + נתונים אלו מאפשרים לנו להבין אילו פונקציות נמצאות בשימוש תכוף ולשפר אותן. אנחנו יכולים גם להעריך כמה זמן צריך לתמוך בטכנולוגיה ישנה יותר ומתי אנחנו יכולים, למשל, להפוך גירסת מערכת הפעלה חדשה יותר לחובה מבלי להשפיע על (יותר מדי) משתמשים. + שפר את האפליקציה + ניתוח אנונימי נשאר מושבת + %s תודה על תמיכתך! + הירשם + נא להזדהות. + הערה לבתי מרקחת: אנו מקבלים את הפרטים ליצירת קשר ומידע על בתי מרקחת מאת mein-apothekenportal.de. האם גילית שגיאה או שברצונך לתקן נתונים? + למד עוד + בתי מרקחת + למרבה הצער זה לא עבד \uD83D\uDE15 + אנא נסה זאת שוב. + נְגִישׁוּת + אפשר התקרבות + להציל + המלצות: %s + כתוב מייל + כאשר אתה שולח את ההודעה שלך, המידע הבא על החומרה ומערכת ההפעלה המשמשת מועבר: + מימוש באתר בלבד + אתה עדיין לא יכול לשלוח מרשמים אלקטרוניים לבית המרקחת הזה. + כרגע פתוח + שירות מסנג\'ר + מִשׁלוֹחַ + לְסַנֵן + לְסַנֵן + שתף מיקום + מובן + התאמות חוזרות ונשנות של סיסמא + שגיאה 20 10 76631 + תעודת כרטיס הבריאות שלך לא תקפה. אולי פג תוקף הכרטיס שלך? נא לפנות לקופת החולים שלך. + ניסיונות התחברות לא מוצלחים + + זוהו %s ניסיונות התחברות לא מוצלחים. + + + זוהו %s ניסיונות התחברות לא מוצלחים. + + אסימונים + אסימוני גישה + אסימוני SSO + אין אסימון גישה זמין + אין אסימון SSO זמין + הועתק ללוח + לחץ כדי להעתיק את האסימון ללוח + ניתן לפדות רק היום + להתיר + אין חיבור לשרת + אנא נסה שוב בעוד מספר דקות + טען שוב + הצג אסימונים + איך תרצו לאבטח את האפליקציה? + הודעה + לא הוגדר גיבוי מכשיר עבור מכשיר זה + אנו ממליצים לך להגן בנוסף על המידע הרפואי שלך באמצעות אבטחת מכשירים כגון קוד או ביומטריה. + אל תציג הודעה זו שוב בעתיד. + חיבור נכשל. לא ניתן היה ליצור חיבור לרשת. + התקשורת עם השרת נכשלה: קוד מצב %s . + התקשורת עם השרת נכשלה: אנא בדוק את חיבור האינטרנט ואת הגדרות השעה/תאריך. + אַזהָרָה + ייתכן שלמכשיר שלך יש אבטחה מופחתת + זה יכול להיגרם, למשל, על ידי התקנים שעברו מניפולציות או כאשר מצב מפתח מופעל. אנו ממליצים לא להשתמש באפליקציה במכשירים שבורים מטעמי אבטחה. + אני מכיר בסיכון המוגבר ועדיין ארצה להמשיך. + מדוע מכשירים עם גישת שורש הם סיכון אבטחה פוטנציאלי? + למד עוד + שם הפרופיל + נא להזין שם לפרופיל החדש. + שם פרופיל + פרופילים + כיצד לזהות כרטיס בריאות התומך ב-NFC + אין אפשרות ליצור קשר דרך האפליקציה הזו + אנא השתמש בערוצים הרגילים כדי ליצור קשר עם חברת הביטוח שלך. + כרטיס בריאות וקוד PIN + PIN בלבד + רישום באפליקציית המרשם האלקטרוני + שדה השם לא יכול להיות ריק. + פרופיל עם השם שהזנת כבר קיים. + פּרוֹפִיל + נבחרו %s + צבע רקע + אפור אביב + טל שמש + זה! האם! וָרוֹד! + עֵץ + ירח כחול ספטמבר + לא מחובר + קשורים ביחד + מחובר לאחרונה ב- %s + מחק פרופיל? + פעולה זו תמחק את כל הנתונים מהפרופיל במכשיר זה. המרשמים שלך ברשת הבריאות יישמרו. + לִמְחוֹק + לְבַטֵל + מחק פרופיל + אתה רוצה למחוק את הפרופיל האחרון. + האפליקציה דורשת לפחות פרופיל אחד. נא להזין שם לפרופיל החדש. + שגיאה 20 10 76831 + לא ניתן היה להגיע אל ספריית כרטיסי הבריאות. בבקשה נסה שוב. + תוכל למצוא מידע מאומת במומחיות על מחלות, קודי ICD ונושאי מניעה וטיפול בפורטל הבריאות הלאומי. + פתח את health.bund.de + שינינו את מדיניות הפרטיות + אפליקציית המרשם האלקטרוני התפתחה. זה הצריך לעדכן את מדיניות הפרטיות שלנו. + פתח את מדיניות הפרטיות + זה השתנה מאז %s : + מה קורה כשפותחים את האפליקציה? + מה קורה אם אני משתמש בפונקציית המצלמה/קורא מתכונים עם המצלמה? + אין מתכונים חדשים זמינים + + המתכון החדש %s + + + %s מתכונים חדשים + + פָּדִי + נמצא בעיבוד + נפדה + לא ידוע + הצג יומני גישה + מי ניגש למתכונים שלך ומתי? + מפתח גישה לשירות המרשם + יומני גישה + אין יומני גישה + אין עדיין יומני גישה. + המתכון נמצא כעת בתהליך ולא ניתן למחוק אותו + לְקַבֵּל + כנראה שזה לא עבד + אנו מודעים לכך שלקשר עם כרטיס הבריאות יש מלכודות. בעתיד, הרישום אמור להיות אפשרי גם באמצעות אפליקציית ביטוח בריאות מאומתת.\n\nאנו פועלים גם להבטיח שניתן יהיה לממש מרשמים באופן דיגיטלי ללא הרשמה.\n\nהאם שמת לב למשהו במהלך התהליך הזה שהיית רוצה לשתף אותנו? אנא כתבו לנו, נשמח גם לקבל משוב ביקורתי מאוד. + טיפים לחיבור + שפר את חוזק החיבור + במידת הצורך, הסר את כיסוי המגן. + אם המכשיר רוטט ואז החיבור מתנתק, חפש את המיקום האופטימלי ברדיוס קטן. + העבר את המכשיר לאט מאוד על פני המפה. + הנח את המכשיר ישירות על הכרטיס. + לשם כך, הנח את כרטיס הבריאות על משטח שטוח (למשל שולחן). + שפר את חוזק החיבור + שימו לב למיקום חיישן ה-NFC + גלה היכן נמצא חיישן NFC במכשיר שלך (כאן, למשל, סקירה כללית של מכשירים מ- %s ). + במקרים מסוימים, המיקום של חיישן NFC יכול להיות שונה בתוך סדרת דגמים (כאן, למשל, המידע עבור %s ). + העצה הבאה + נוסף + סגור + לְנַסוֹת + כתוב לנו + חיפוש בית מרקחת ברישיון + לִפְדוֹת + מתכון סרוק + נסרק ב- %s + סומן כמומש ב- %s + איך אתה רוצה להמשיך? + שלח לבית מרקחת + זמין בקרוב + הזמן עכשיו לאיסוף או שלח אותו באמצעות שליח או משלוח + שמור להזמנה מאוחרת יותר + שמור מתכונים במכשיר + + המשך עם מתכון %s + + + המשך עם %s מתכונים + + חיבור כרטיס בריאות נכשל + הפרופיל הנוכחי כבר מקושר לכרטיס בריאות אחר (מספר קופת חולים %s ). + כרטיס הבריאות שלך כבר מקושר לפרופיל אחר. עבור לפרופיל %s . + להציל + פרטי התקשרות וכתובת + איש קשר + מספר טלפון + כתובת אימייל (אופציונלי) + כתובת למשלוח + שם פרטי ושם משפחה + מספר רחוב ובית + כתובת נוספת (אופציונלי) + הוראות משלוח (לא חובה) + נדרשים פרטים נוספים ליצירת קשר + בטל שינויים? + להשליך + לצורך החיפוש, ספריית בית המרקחת משתמשת בקואורדינטות גיאוגרפיות שנקבעו בעזרת OpenStreetMap. אנו מודים לפרויקט על העזרה הזו. + © OpenStreetMap ( %s ) + https://www.openstreetmap.org/copyright + הגנת מידע ושימוש + נוסף + היה עליך להזמין באופן פעיל את ה-PIN של כרטיס הבריאות שלך מחברת ביטוח הבריאות שלך ולאחר מכן לקבל אותו בתהליך מאובטח כגון Postident. + לא התקבל PIN + קוד סודי + בדוק את חיבור האינטרנט של המכשיר ואת הגדרות השעה/תאריך. + ננעל בחוץ? אנא אמת את האישורים הביומטריים שלך במכשיר זה. + אזור עזרה + גודל אריזה ויחידה + רכיב פעיל + כמות החומר הפעיל + שם אצווה + Exp + קטגוריה + תַרכִּיב + לְקַבֵּל + בטל + הודעה + עזרו לנו לשפר את האפליקציה הזו + הסיסמה גלויה + הסיסמה אינה גלויה + ביומטריה + סיסמה + ממתין לתשובה + אין מתכונים + אין לך כרגע מרשמים שניתנים למימוש. + לעדכן + יציאה אוטומטית + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. התחבר מחדש כדי לקבל מתכונים עדכניים. + לְחַבֵּר + קיבלתם תדפיס נייר? + הוסף מתכונים לרשימה שלך על ידי הקשה על כפתור הסריקה בפינה השמאלית העליונה. + סרוק תדפיס נייר + כדי לקבל מרשמים אוטומטית, עליך להיות מחובר. + הירשם + ללא מרשמים מומשים + המרשמים המומשים שלך מוצגים כאן. מטעמי הגנה על נתונים, המתכונים שלך יימחקו משרת המתכונים לאחר 100 ימים. + ללא מרשמים מומשים + המרשמים המומשים שלך מוצגים כאן. הוסף מתכונים באמצעות סריקה כדי להתחיל לממש. + ניהול מכשירים + מכשירים מחוברים + רשום מאז %s (מכשיר זה) + רשום מאז %s + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. + קוד סודי + הזן PIN (כרטיס בריאות). + נוסף + הירשם + מכשירים מחוברים + הסר מכשיר? + לְבַטֵל + לְהַסִיר + להסיר את המכשיר הזה? + האם ברצונך להסיר %s ? + אם תסיר %s , החיבור לשרת המתכונים ינותק לצמיתות תוך 12 שעות לכל המאוחר. + המכשירים נטענים... + אין מכשירים + אין מכשירים מחוברים לכרטיס בריאות זה. + נסה שוב + או - או :-( + לא ניתן היה לטעון את רשימת ההתקנים. + wwweg… + אין חיבור אינטרנט. + תרופות וחבישות + סמים + חלוקת תרופות מרשם בהתאם לסעיף 4 AMVV + אתה צריך עזרה? + ריכזנו עבורכם כמה טיפים לפתרון הבעיות הנפוצות ביותר. + התחל טיפים לחיבור + לבטל נעילה + כרטיס חסום + ה-PIN הוזן שגוי שלוש פעמים. לכן הכרטיס שלך נחסם לשימוש עם PIN מטעמי אבטחה. + בטל את נעילת הכרטיס + הזן PUK + עם ה-PIN שלך קיבלת מחברת הביטוח שלך קוד PUK בן 8 ספרות. + בחר PIN חדש + אתה יכול לבחור את ה-PIN החדש שלך בעצמך (6 עד 8 ספרות). + זכרתם את ה-PIN שלכם? + אנא רשום את ה-PIN שלך ושמור אותו במקום בטוח. + לְבַטֵל + בסדר + פתיחה לא אפשרית + הגעת למספר המרבי של ביטול נעילה של כרטיס עם PUK זה או שהזנת אותו שוב ושוב באופן שגוי. אנא פנה לחברת הביטוח שלך. + אתה יכול להשתמש ב-PUK אחד עבור עד 10 ביטולי נעילה. + הכרטיס לא נעול + מה אתה צריך: + כרטיס הבריאות שלך + PUK של כרטיס הבריאות שלך + נוסף + כרטיס בריאות + הזמנת PIN או כרטיס + הירשם + איך תרצה לאמת את עצמך? + כרטיס בריאות מאופשר NFC + PIN עבור כרטיס הבריאות + אין לך עדיין כרטיס בריאות ו-PIN התומכים ב-NFC? + הגש בקשה עכשיו + או: היכנס עם %s . + אפליקציית ביטוח הבריאות שלך + "תוכל למצוא את מספר הגישה שלך בפינה השמאלית העליונה של כרטיס הבריאות שלך." + לכרטיס שלי אין מספר גישה + + יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. + + + יש לך %s ניסיונות נוספים לפני שהכרטיס שלך ייחסם לשימוש ב-PIN. + + הנח את כרטיס הבריאות בגב הטלפון + התהליך הבא עשוי להימשך עד 30 שניות. + הנח את הכרטיס %s בגב הטלפון. + באזור הימני העליון + באמצע באזור העליון + באזור השמאלי העליון + באזור האמצעי מימין + אֶמצַע + באזור האמצעי משמאל + באזור הימני התחתון + באמצע באזור התחתון + באזור השמאלי התחתון + עֶזרָה + נשלח לפני %s דקות + נשלח ב- %s + נשלח רק עכשיו + נשלח בזמן %s + לא תקף יותר + משוב מאפליקציית המרשם האלקטרוני + אנו מצפים למשוב שלך. אנא השתמש במקום הבא ודייק ככל האפשר: + PUK + סגור + חבל… + למרבה הצער, המכשיר שלך אינו עומד בדרישות המינימום לרישום באפליקציית המרשם האלקטרוני. לצורך אימות מאובטח עם כרטיס הבריאות שלך, נדרשים לפחות אנדרואיד 7 ושבב NFC. + למד עוד + לשמור נתוני כניסה? + להציל + אל תחסוך + הודעה + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. + הגדר אבטחה ביומטרית + לא ניתן לשמור נתוני גישה. לפני כן, הגדר אבטחה ביומטרית (למשל טביעת אצבע) במכשיר שלך. + לְבַטֵל + הגדרות + הודעה + לְקַבֵּל + אבטחת נתוני המרשם שלך + \אפליקציה זו משתמשת בחיישן הביומטרי המאובטח ביותר שסופק על ידי המכשיר שלך כדי לאבטח את האישורים שלך באזור מוגן של אחסון המכשיר.\ + האבטחה הביומטרית של נתוני הגישה שלך מאפשרת לך לפתוח את האפליקציה הזו בעתיד, להציג, לאחזר, לממש או למחוק מרשמים ללא כרטיס בריאות והקלדת ה-PIN שלך. + אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. + שלצערי לא עבד + האימות עם אפליקציית ביטוח הבריאות לא הצליח. + פג תוקף ב- %s + המתכון כבר נמחק מהשרת + אנא תקן את הערך שלך או בטל שינויים + נכון + נתוני מבוטח + שֵׁם מִשׁפָּחָה + ביטוח + מספר ביטוח + מספר גישה + הירשם + להתנתק + להציל + שינוי + ערוך תמונת פרופיל + נוסף + השרת לא מגיב + בבקשה נסה שוב מאוחר יותר. + נסה שוב + חפש ביטוח + להתחבר לשרת המתכונים עכשיו? + התחברת בהצלחה + החיבור אבד + להתחבר לשרת המתכונים עכשיו? + אין אסימונים + תקבל אסימון כאשר תיכנס לשירות המרשם. + בחר את ה-PIN הרצוי + בטל את נעילת הכרטיס + בחר PIN + חזור על PIN + הערכים שונים זה מזה. + אין פקודות + עדיין אין לך הזמנות. + זֶה עַתָה + בשעה %s + עגלת הקניות מוכנה + המתכון התווסף לעגלת הקניות שלך. נא להיכנס לאתר בית המרקחת להשלמת ההזמנה. + פתיחת עגלת קניות + הצג את קוד האיסוף הזה בבית המרקחת. + קוד איסוף התקבל + אין אפשרות להציג את ההודעה + אנא צור קשר עם בית המרקחת שלך ( %s ). + קישור לבית מרקחת + הצג קוד איסוף + הצג את ההודעה + %s בשעה %s + המתכון נשלח אל %s . בחלק מבתי המרקחת אין עדיין אפשרות תגובה דיגיטלית. אם לא תהיה תגובה עד מחר, נא להתקשר כאמצעי זהירות. + סקירת הזמנה + חָדָשׁ + קוּרס + הסדר + ללא תשלום עבור המתקשר. זמני השירות: שני - שישי 8:00 בבוקר - 20:00 בערב למעט חגים פדרליים + בֵּית מִרקַחַת + בחר את ה-PIN הרצוי + ה-PIN הרצוי נשמר + כרגע פתוח וקרוב אליי + סנן לפי… + לְחַפֵּשׂ + משימה ישירה + בתי מרקחת + מספר טלפון (אופציונלי) + לְחַפֵּשׂ + אין מידע חוקי של בית מרקחת + לא נמצא מידע עדכני על בית מרקחת זה. הערך של בית מרקחת זה יימחק. + בסדר + ספריית בתי המרקחת אינה זמינה + נכון לעכשיו, לא ניתן לגשת למידע עדכני על בית מרקחת זה. אנא בדוק את חיבור האינטרנט שלך. + לְבַטֵל + נסה שוב + שמור סביבה + לא ניתן להיכנס + נראה שמאפייני הכניסה הביומטריים שלך השתנו. אנא היכנס שוב עם כרטיס הבריאות שלך. + לְבַטֵל + הירשם + פרופיל 1 + קרוב אליי + ניתן למימוש מאוחר יותר + ניתן למימוש מ %s + שיפורים במוצר + ניתוח אנונימי + עזרו לנו לשפר את האפליקציה הזו. כל נתוני השימוש נאספים בעילום שם ומשמשים אך ורק לשיפור חווית המשתמש. + הגדרות אישיות + נְגִישׁוּת + שיפורים במוצר + נוסף מתכון + המתכון כבר זמין + אירעה שגיאה במהלך הייבוא + מספר גישה + לִמְחוֹק + מתכון סרוק + נשכח PIN + + %s מתכון + + + %s מתכונים + + קראתי ומסכים למדיניות הפרטיות ותנאי השימוש. + הגנת מידע + תנאי שימוש + אנחנו נהיה: + שפר את השימושיות. + זיהוי שגיאות וקריסות. + כמובן, אסוף את כל הנתונים בעילום שם. + אתה יכול לשנות החלטה זו בכל עת בהגדרות המערכת. + לְהַמשִׁיך + לְקַבֵּל + האפליקציה משתמשת בשיטה הבטוחה ביותר שהגדרת במכשיר שלך. + להציל + בחר + תְרוּפָה + שם מסחרי + כן + לא + מִנוּן + יום הוצאה לאור + מרשם זה ימומש עבורך במסגרת טיפול. + לא מוגדר + תשלום נוסף + תְרוּפָה + הוראות לשימוש + זכאי לפי BVG + הכנה חלופית + שם המתכון + אריזה + הוראות ייצור + תיאור + ניתנו על ידי + הונפק ב: + רכיב פעיל + רשום + לְקַבֵּל + מהי משימה ישירה? + עם הפניה ישירה, מרשם מרופא או בית חולים ממולא ישירות בבית מרקחת. מבוטחים אינם חייבים לבצע כל פעולה ואינם יכולים להתערב בתהליך הפדיון. \n\n הפניות ישירות מופיעות באפליקציית המרשם האלקטרוני כדי להפוך את הטיפול שלך לשקוף יותר עבורך. + עמלת שירות חירום + אם מרשם ממולא בין השעות 20:00-06:00 או בימי ראשון ובחגים, ייתכן שתחויב בתשלום נוסף של 2.50 יורו. + תרופות בעלות השתתפות עצמית + פטור מתשלום נוסף + אנשים עם ביטוח בריאות סטטוטורי משלמים בדרך כלל מקסימום 10 אירו עבור תרופות מרשם. עמלות גבוהות יותר עשויות לחול אם מתבקשת תרופה מיצרן ספציפי שאינה מכוסה בהסכם הנחה של ביטוח בריאות (\"בקש תרופה\"). \n\n ילדים וצעירים מתחת לגיל 18 פטורים מתשלומים נוספים. \n\n אם מימוש מרשמים לאחר 28 ימים לאחר הוצאתם, יש לשאת במלואן בעלויות. \n\n אם אתה מוציא הרבה תרופות במהלך השנה, אתה יכול להגיש בקשה לפטור מתשלום ההשתתפות העצמית מקופת החולים שלך + אתה פטור מתשלום השתתפות עצמית עבור תרופה זו. קופת החולים שלך תכסה את עלות התרופה. + לכמה זמן מרשם זה תקף? + במהלך תקופה זו תוכל לממש את המרשם שלך בכל בית מרקחת בתשלום נוסף של 10 אירו. + מתכון סרוק + מרשמים המיובאים מעותק קשיח אינם יכולים להציג מידע אישי או רפואי מסיבות אבטחה. \n\n היכנס לאפליקציה זו באמצעות כרטיס בריאות או אפליקציית ביטוח כדי לראות את כל המידע הכלול במרשם. + מתכון שגוי + מרשם זה הוצא בצורה שגויה. + עמלת שירות חירום + מינון לפי הוראות כתובות + טלפון + אתר אינטרנט + דוֹאַר + שתף את מיקומך כדי למצוא בתי מרקחת בקרבת מקום. + בסדר + הזן PIN נוכחי + הוזן PIN שגוי + ה-PIN הנוכחי של כרטיס הבריאות שלך + כרטיס חסום + בטל את נעילת הכרטיס שלך בהגדרות > ביטול נעילת כרטיס. + מטעמי אבטחה, אנא הזן את ה-PIN הנוכחי שלך. + נשכח PIN + מתכון לא נכון + תְרוּפָה + נראה שמשהו השתבש במהלך יצירת המתכון שלך. לדווח על שגיאה? + להגיש תלונה + לא מחובר + רשום עם + כרטיס בריאות + ביומטריה + לא מחובר + הערת אזהרה + בית מרקחת נוסף למועדפים + בית המרקחת הוסר מהמועדפים + בתי המרקחת שלי + פעולת הכתיבה לא הצליחה + לא ניתן היה לשמור את ה-PIN + להגיש תלונה + הקצה PIN + כלל הגישה הופר + אין לך הרשאה לגשת לספריית המפות. + הקצה סיכה משלך + הכרטיס מאובטח ב-PIN מקופת החולים שלך (PIN תחבורה) נא להזין PIN משלך. + הסיסמה לא נמצאה + אין סיסמה מאוחסנת בכרטיס שלך. + נותקת מהמערכת + היכנס שוב כדי לעדכן את המתכונים שלך. + מספר החומר הפעיל + עוצמה ואחדות + מומש לפני %s דקות + מומש ב- %s + נפדה רק עכשיו + מומש בשעה %s + מרשם זה מולא כחלק מטיפול עבורך. + עמלת שירות חירום + לא ניתן למלא מרשם זה בבית מרקחת בלילה ללא תשלום נוסף של דמי שירות חירום. + חפש כאן + הגדרות + שתף מיקום בהגדרות. + קרוב אליי + לחץ והחזק כדי לערוך את השם. + הזן את השם החדש עבור הפרופיל. + כדי לקבל מרשמים באופן דיגיטלי מהמרפאה שלך, עליך להיות מחובר. + מקבלים מרשמים בצורה דיגיטלית? + משוך למטה את המסך כדי לרענן. + אין מתכונים + הירשם לקבלת מתכונים באופן אוטומטי. + הירשם + ארכיון מתכונים + אולי אחר כך + הירשם + ערוך תמונת פרופיל + ארכיון מתכונים + הכנס שם + להציל + ההזמנה שלי + נמען: ב + מתכונים + בֵּית מִרקַחַת + לִשְׁלוֹחַ + שינוי + איסוף בבית המרקחת + משלוח באמצעות שליח + משלוח בהזמנה בדואר + %s מתכונים + לא ניתן לפדות + לא ניתן היה לממש מרשם אחד או יותר. + לא נבחר מתכון + כדי לממש מתכונים, יש לבחור לפחות מתכון אחד. + הוסף פרטים ליצירת קשר + שינוי + אין מתכון + אין לך כרגע מרשמים שניתנים למימוש + אוסף + נער שליחויות + מִשׁלוֹחַ + בחר מתכונים + הקש כאן כדי לסרוק מתכונים + לחץ לחיצה ארוכה כדי לערוך שמות + הוסף פרופילים נוספים, למשל עבור הילדים או ההורים שלך + לחץ על התצוגה כדי לדלג על הסבר הכלי שמופיע. + איך לפדות? + כיצד תרצה לקבל את התרופה שלך? + הצג קוד + תסרוק את זה בבית המרקחת + שלח לבית מרקחת + הזמן או שלח אותו + מוּכָן + קוד איסוף + קודים בודדים + + יש לך מתכון %s . + + + יש לך %s מתכונים. + + לעשות בחירה + כל המתכונים + איזה מתכונים? + נוסף + נוסף + למד עוד + הודעה + אפליקציה זו משתמשת בתוכנה מגוגל כדי לזהות קודים. + למד עוד + מידע על סורק קוד המתכון + אילו נתונים מכיל קוד המתכון? + קוד המתכון מכיל רק מזהה עבור המתכון. המשמעות היא שניתן למצוא את המרשם בשירות המרשם ברשת הבריאות הדיגיטלית. קוד המרשם אינו מכיל מידע אודותיך או אודות התרופה שלך. + אז אף אחד לא יכול לעשות כלום רק עם קוד המתכון? + נכון. יש להוריד את נתוני המרשם משירות המרשם. לשם כך נדרשת התחברות מאובטחת. + מי יכול להירשם לשירות המרשם? + הרשמה לשירות המרשם ברשת הבריאות הדיגיטלית אפשרית למבוטחים, בתי מרקחת, מרפאות ובתי חולים. + מדוע אפליקציית המרשם האלקטרונית משתמשת בתכונות Google? + גוגל מציעה פונקציות שניתן לשלב בקלות באפליקציות ושגוגל מפתחת ומעדכנת ללא הרף. זה מבטיח שהפונקציות פועלות על מכשירים רבים ושונים וניתן להפעיל אותן בבטחה. האפליקציה משתמשת בתכונה כדי לשפר את פונקציונליות המצלמה והסריקה עבור מכשירי אנדרואיד (Google ML Kit). + כיצד פועל שיפור הסריקה עם Google ML Kit? + ערכת Google ML עוזרת לייעל את התמונה שצולמה על ידי מצלמה כך שניתן לקרוא את קודי המתכון גם בתנאי תאורה גרועים או עם דגמי מצלמה ישנים יותר. + האם הנתונים על המרשם או התרופה שלי ישותפו עם Google? + לא. קוד המתכון שנקרא נשמר ישירות באפליקציה. זה לא ישותף עם גוגל. נתוני המרשם אינם נשמרים בקוד, אלא רק ברשת הבריאות הדיגיטלית. משם הם מועברים לאפליקציה. לגוגל אין גישה לרשת הבריאות הדיגיטלית. + אילו נתונים מעבדת Google בעת שימוש ב-ML Kit? + גוגל מקבלת גישה רק למידע טכני על המכשיר בו נעשה שימוש והשימוש הכללי בפונקציה הנוספת (למשל שיעור שגיאות, הגדרות מצלמה) על מנת לתעד זאת באופן סטטיסטי ובכך לשפר את הפונקציה הנוספת. בעת הגישה, Google מתעדת זמנית את כתובת ה-IP של המכשיר שלך. מידע עליך ועל תוכן המתכון אינו מתועד על ידי גוגל. + האם השימוש ב-Google ML Kit הוא התנדבותי? + כן. עם זאת, ML Kit מובנה בסורק קוד המתכון בגרסת האנדרואיד של אפליקציית המרשם האלקטרוני. אם אתה משתמש בסורק קוד המתכון במכשיר אנדרואיד, תמיד נעשה שימוש בפונקציית ML Kit. עם זאת, אתה יכול להימנע משימוש בסורק קוד המתכון. ניתן לטעון את המרשמים שלך לאפליקציה גם אם תיכנס לרשת הבריאות הדיגיטלית עם כרטיס הבריאות האלקטרוני או דרך אפליקציית קופת החולים שלך. + האם אני יכול לראות מי צפה במתכונים שלי? + כן. כל הגישה לנתונים שלך מתועדת במלואה ברשת הבריאות הדיגיטלית. באפליקציית המרשם האלקטרוני תוכל לראות מי ניגש לנתונים שלך. + לאן אוכל לפנות אם יש לי שאלות לגבי האפליקציה או המרשם האלקטרוני? + מידע מפורט ניתן למצוא בהצהרת הגנת המידע. + מספר החבילות שנקבעו + אין מתכונים + בשביל זה אתה צריך מרשמים שניתנים לפדיון. + בחר ביטוח + חפש ביטוח + לְבַטֵל + על מה תרצה להגיש בקשה? + עבור אפליקציה זו אתה צריך כרטיס ואת ה-PIN המשויך. + כיצד תרצה ליצור קשר עם חברת הביטוח שלך? + חברת הביטוח שלך מציעה את אפשרויות ההתקשרות הבאות + חברת הביטוח שלך מציעה את אפשרות ההתקשרות הבאה + סגור + PIN הוזן שגוי. + מספר הגישה הוזן שגוי + PUK הוזן בצורה שגויה. + קבלות עלויות + הצג קבלות עלויות + קבלות עלויות + כדי לקבל קבלות על הוצאות, עליך להיות מחובר לשרת. + לְחַבֵּר + ללא קבלות עלויות + השבת + לְבַטֵל + השבת את הפונקציה + פעולה זו תמחק את כל קבלות ההוצאות מהמכשיר הזה ומהשרת. + קבלת קבלות עלויות + קבלות העלות שלך מאוחסנות גם בשרת המתכונים. + לְהַפְעִיל + סה\"כ: %s %s + בחר + לְפַצֵל + לִמְחוֹק + לִמְחוֹק + שלח + %s € + מחיר סופי + טיפ: שלח קבלות עלות דרך אפליקציית הביטוח + שלח קבלות עלות בקלות באמצעות האפליקציה של חברת הביטוח שלך. בשלב הבא, בחר את האפליקציה הזו ולחץ על שתף. + תרגול + בֵּית מִרקַחַת + תַאֲרִיך + להראות יותר + מזהה תרופה + הונפק עבור + KVNR: %s + נולד ב: %s + בסדר + איך מגישים מסמכים תומכים? + העבר ישירות לאפליקציה של משרד הביטוח/תגמולים שלך. כדי לעשות זאת, בחר את האפליקציה בעמוד הבא. + אוֹ + שמור את הקובץ וייבא מאוחר יותר לפורטל הביטוח/תגמולים. + מאמר: %s + ספירה: %s + מע\"מ: %s %% + מחיר ברוטו באירו: %s + עמלות נוספות + עמלת שירות חירום + עמלת BTM + דמי מרשם T + עלויות רכש + שירות מסנג\'ר + סך הכל באירו: %s + לִגבּוֹת + באמת למחוק? + הקובץ יימחק מהמכשיר שלך ומהשרת. + לִמְחוֹק + פורסם + מיקוד + מקום + יגאל עבורך + נגאל עבורך + עליך להיות מחובר כדי להשתמש בשירות זה. + כרטיס בריאות + נדרש PIN משויך + ניתן לממש רק מחר כמשלם עצמי + נותרו רק %s ימים למימוש בתור משלם עצמי + עדיין ניתן לפדות כתשלום עצמי למשך %s ימים + נותרו רק %s ימים למימוש + עדיין ניתן למימוש למשך %s ימים + ניתן למימוש רק מחר + חיובים חלים + לוקח ביטוח + המתכון/ים הועברו בהצלחה. + לא ניתן לעבד את המתכון. בבקשה נסה שוב. ייתכן שתצטרך לבחור בית מרקחת אחר. + לא ניתן לעבד את המתכון. בית המרקחת מדווח על שגיאה לא ידועה. במידת הצורך, נסה בית מרקחת אחר. + המרשם נדחה על ידי בית המרקחת. ייתכן שהמרשם אינו חוקי או שכתובת המשלוח או פרטי הקשר שלך אינם חוקיים. + נסה שוב ואולי תבחר בית מרקחת אחר. אם השגיאה נמשכת, אנא הודע לתמיכה. + המתכון הועבר בהצלחה. עם זאת, בית המרקחת מדווח על טעות בעיבוד. נא לפנות לבית המרקחת. + המרשם נדחה על ידי בית המרקחת. המרשם כבר מומש. + המרשם נדחה על ידי בית המרקחת. המתכון נמחק. + לא ניתן היה להעביר את המתכון. אנא בדוק את חיבור האינטרנט שלך ונסה שוב. + לא ניתן היה להעביר מתכון אחד או יותר. + שגיאה בשליחת + נשלח בהצלחה! + טעות בבית המרקחת + טעות בבית המרקחת + פנה לבית מרקחת + מרשם כבר מומש + המתכון נמחק + אין אינטרנט + כדי לקבל יומני גישה, עליך להיות מחובר לשרת. + אתה עדיין יכול למלא את המרשם בבית מרקחת בתוך תקופה זו, אך תצטרך לשלם את כל מחיר הרכישה עבור התרופה בעצמך. לחלופין, אתה יכול לבקש מהמרפאה שלך להנפיק מחדש את המרשם. + מוּכָן + בקש תיקון + בבית המרקחת + באפליקציה + מסור את הקוד הזה בבית המרקחת שלך. + בקשה לתיקון חיוב + תְרוּפָה + אנא הזן לפחות תו אחד. + או: נסה את האפליקציה במצב הדגמה + במצב הדגמה + במצב הדגמה + השתמש במצב הדגמה + מצב הדגמה הופעל + סיים כאן + הפעל מצב הדגמה + לא עכשיו + לחפש + קבלת קבלות עלויות באופן דיגיטלי + לאחר הפעלת קבלות העלות, תמצא אותן כאן לאחר מימוש המרשם שלך. + לְהַפְעִיל + קבלת עלות דיגיטלית + ברגע שבית המרקחת הפקיד את קבלה העלות היא תופיע כאן. + קבלת עלות + קבלת קבלות עלויות באופן דיגיטלי + הערה: לא תקבל יותר את קבלות העלות שלך כתדפיס בבית המרקחת. + לְהַפְעִיל + אולי אחר כך + הֶסכֵּם + בהסכמתך, תמנע מלהדפיס אותם בבית המרקחת ותגיש את קבלות העלות שלך באופן דיגיטלי בעתיד. + לְבַטֵל + מוסכם + צא ממצב הדגמה + לסקירה כללית של קבלות העלויות + אל קבלות העלות + כעת אתה מקבל קבלות עלות באופן דיגיטלי + קבלות עלויות כבר תקבלו בדיגיטל + בסדר + נסה שוב + הפונקציה מופעלת + אתה כבר יכול לקבל קבלות עלויות באופן דיגיטלי. + אין אינטרנט + אין חיבור לאינטרנט. + בקשה לא נכונה + הייתה בעיה עם הבקשה. אנחנו עובדים על פתרון. + השרת לא מגיב + אנא נסה שוב בעוד מספר דקות. + ההתחברות נכשלה + כרגע לא ניתן לאחזר מידע. בבקשה נסה שוב מאוחר יותר. + נִדחֶה + השרת דחה את בקשתך. בבקשה נסה שוב מאוחר יותר. + עדכן את האפליקציה + כדי להשתמש בתכונה זו, אנא עדכן את האפליקציה שלך. + ההתחברות נכשלה + לשרת הייתה בעיה בכניסה. נא להיכנס שוב. + הירשם + העתק כתובת URL + התחבר לאפליקציית ביטוח בריאות חיצונית. + לא ניתן למצוא את אפליקציית ביטוח הבריאות + לא ניתן היה לגשת לאפליקציית ביטוח הבריאות. האימות לא הצליח. + לא אפשרי עבור ביטוח בריאות זה + אנא ספק מספר טלפון ליצירת קשר. + אנא ספק שם פרטי ושם משפחה כדי ליצור איתנו קשר. + אנא ספק רחוב ומספר בית כדי ליצור איתנו קשר. + אנא ספק את המיקוד שלך כדי ליצור איתנו קשר. + אנא ציינו את מקום מגוריכם כדי ליצור איתנו קשר. + מספר הטלפון שהזנת אינו חוקי. (תווים קצרים מדי, ארוכים מדי, לא חוקיים) + אנא הזן כתובת אימייל כדי ליצור איתנו קשר + כתובת האימייל שהוזנה אינה חוקית. + הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) + הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) + הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) + הטקסט שהזנת אינו חוקי. (ארוכים מדי, קצרים מדי, תווים לא חוקיים) + הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) + הטקסט שהזנת אינו חוקי. (ארוכים מדי, תווים לא חוקיים) + נתונים שגויים - נדרש תיקון + נדרשת אפליקציה נוספת + מוּמלָץ + מזהה בריאותי דיגיטלי + לשנות הגדרות? + אנא הפעל קישורי פתיחה בהגדרות שלך כדי להמשיך. + פתח את ההגדרות + גרסת אפליקציה מיושנת + הגרסה שלך של האפליקציה מיושנת ואינה נתמכת עוד. כדי להמשיך להשתמש במרשם האלקטרוני, אנא עדכן את האפליקציה. + לעדכן + מספר הגישה שלך הוא באורך %s ספרות. + ה-PIN שלך יכול להיות באורך של %s עד %s ספרות. + לאסוף + שָׁלִיחַ + מִשׁלוֹחַ + נדרש סמארטפון תומך NFC + לא הצלחנו לעבד את הבקשה למחיקת המתכון. אנו עובדים על פתרון ( %s ). + משהו השתבש בזמן מחיקת המרשם שלי, קיבלתי את קוד השגיאה %s . אנא הודע לי כאשר המחיקה תתאפשר שוב. + נוסף + בקשה לא נכונה + להגיש תלונה + מחק מקומית + אין אינטרנט + לא ניתן היה למחוק את המתכון. אנא בדוק את חיבור האינטרנט שלך ונסה שוב. + נסה שוב + בסדר + התחברות מחדש + עליך להיות מחובר כדי למחוק את המתכון (401). + הירשם + לְבַטֵל + מחיקה בלתי אפשרית + אינך רשאי למחוק את המרשם בשלב זה מכיוון שהוא נמצא בבית המרקחת למילוי או שמדובר בהקצאה ישירה ממתינה (403). + יותר מדי בקשות + ניסית למחוק את המתכון יותר מדי פעמים. אנא נסה שוב מאוחר יותר (429). + מחק את המתכון + אופס... + "אירעה שגיאת שרת בלתי צפויה. אנא נסה שוב מאוחר יותר (500)." + אבטחת אפליקציה אינה אפשרית. לפני כן, הגדר אבטחה ביומטרית (למשל טביעת אצבע) במכשיר שלך. + גיבוי ביומטרי אינו אפשרי עבור מכשיר זה מכיוון שהוא אינו נתמך על ידי המכשיר שלך. + סַכָּנָה! הטלפון שלך לא מוגן היטב. + פרטי הכניסה שלך נשמרים בטלפון שלך. כדי להגן על הגישה, נעשה שימוש בשיטת הכניסה המועדפת עליך. שיטת התחברות זו, למשל דפוס החלקה, אינה מאובטחת במיוחד. אתה משתמש בתכונה \"שמור פרטי כניסה\" על אחריותך בלבד. + אם אתה עדיין משתמש בפונקציית \"שמור נתוני התחברות\", תוכל בעתיד לצפות, לגשת, לממש או למחוק מרשמים באמצעות אפליקציית המרשם האלקטרוני ללא תעודת בריאות והקלדת קוד PIN. + המיקום לא נמצא + שלח לבית מרקחת + הצג קוד + שפה + שפה + גֶרמָנִיָת + עֲרָבִית + בולגרית + צ\'כית + דַנִי + אנגלית + צָרְפָתִית + עִברִית + אִיטַלְקִית + הוֹלַנדִי + פולני + רומנית + רוּסִי + טורקי + אוקראינית + בְּרִירַת מֶחדָל + קבלות עלות דיגיטליות בוטלו ונמחקו. + "פורום מרשם אלקטרוני" + אין אפשרות להכנה חלופית + הכנה להחלפה אפשרית + הכנת תחליף (Aut idem) + רוקחים מחויבים לתת עדיפות להוצאת תרופות שבגינן סיכמה קופת החולים של החולה הסכם הנחה עם יצרני התרופות. זה לא חל רק אם הרופא מוציא את \"Aut idem\" במרשם, וזה לא המקרה עם המרשם שלך. + הרופא שלך קבע שאתה צריך לקבל את התרופה שנקבעה. אסור לבית המרקחת לבצע החלפה על בסיס הסכם הנחה (\"Aut idem\"). + אפשר צילומי מסך + אם תאפשרו צילומי מסך, העמוד האחרון שפתחת יישאר גלוי ברקע כשתחליף אפליקציות. במידת הצורך, ייתכן שהנתונים האישיים שלך יהיו גלויים. לכן אנו ממליצים לא לאפשר צילומי מסך. + להתיר + לְבַטֵל + החלף את המקלדת שלך למדבקות או השתמש באימוג\'י לתמונת הפרופיל שלך. + בחר תמונת פרופיל + איך אתה רוצה להמשיך? + בחר תמונה + מַצלֵמָה + אימוג\'י + לְבַטֵל + פתח את ההגדרות + להשתמש + לצלם תמונה + ערוך תמונת פרופיל + ההנחה לפני %s דקות + התקבל ב %s + פשוט התקבל + התקבל בשעה %s + HealthID + בחר ביטוח + אם ההרשמה עם מזהה הבריאות לא מתנהלת כמצופה, אנא עקוב אחר הטיפים ממרכז העזרה שלנו. + עֶזרָה + עֶזרָה + טיפים להרשמה באפליקציית הביטוח + חברת הביטוח שלך אחראית לתעודת הבריאות. אנא צור איתם קשר אם יש לך שאלות לגבי ההרשמה. הנה כמה טיפים בדוקים: + שימו לב כי בהתאם לביטוח שלכם, ייתכן שתידרש אפליקציה נפרדת. אנא בדוק עם חברת הביטוח שלך כדי לברר במה מדובר. + הפעל את אפליקציית הקופה והיכנס לשם פעם אחת לפני שתתחיל להירשם באפליקציית המרשם האלקטרוני. + מעבר בין רישום עם תעודת הבריאות לכרטיס הבריאות עלול להוביל לבעיות. לכן, אנא צא באופן פעיל מהפרופיל שלך לפני שתשנה את אפשרות הכניסה. + אם הביטוח שלך אינו ברשימה, תוכל לחילופין להירשם עם תעודת הבריאות שלך וה-PIN המתאים. + אם אפליקציית הקופה לא מפנה אותך בחזרה לאפליקציית המרשם האלקטרוני, אנא הקפד לדווח על שגיאה זו לחברת הביטוח שלך. + אם לא ניתן לגשת לאפליקציית הביטוח, ייתכן שיהיה מועיל לעבור להגדרות הדפדפן של הטלפון החכם ולאפשר פתיחת קישורים. + אם הסמארטפון שלך מריץ אנדרואיד 14, ייתכן שיהיה מועיל לאפשר פתיחת קישורים בהגדרות. + פתח את ההגדרות + שלצערי לא עבד + בבקשה נסה שוב מאוחר יותר. + הודעות שגיאה בעת טעינה. + אנא בחר אימוג\'י או טקסט + להציל + אנא התחבר על מנת להמשיך + נוסף + שִׂיחָה + דוֹאַר\n לִכתוֹב + לבית המרקחת + בֵּית מִרקַחַת + היום + מָחָר + מַסלוּל\n כאן + מחיקת מרשם וקבלה על עלות + אם תמחק את המתכון, גם קבלת העלות הקשורה נמחקת. + הודעות מכל הפרופילים מוצגות כעת יחד + תכונות חדשות + תכונות חדשות + אנו עובדים כל הזמן על תכונות חדשות. שתף אותנו בדעה שלך ועזור לנו לבנות אפליקציה טובה יותר. + הזמנות לכולם + תחת הזמנות כעת תוכל לראות את ההזמנות עבור כל הפרופילים יחד ולצפות בהן הרבה יותר בקלות + + אין החזר הוצאות + ככלל, ביטוח הבריאות אינו מכסה את העלויות עבור מרשם זה. כמטופל, לכן אתה אחראי לתשלום מלא. יש לבדוק באופן פרטני האם העלויות יוחזרו במסגרת ביטוחים נוספים או תגמולים סטטוטוריים. + הביטוח שלך לא יכסה כל עלויות עבור מרשם זה. + + "הביטוח שלך לא יכסה את המרשם %s " + + + "הביטוח שלך לא יכסה את המרשמים %s ." + + נגרם + תַאֲרִיך + תְאוּנָה + תאונת עבודה + מחלה תעשייתית + חֲדָשׁוֹת + אין חדשות + עדיין אין לך הודעות. + 🎉 ההזמנה שלך מוכנה לאיסוף. אנא הצג את קוד האיסוף הזה כדי להזדהות. + למרבה הצער, ההודעה מבית המרקחת שלך הייתה ריקה. אנא צור קשר עם בית המרקחת שלך. + בית המרקחת סיפק לך קישור. + חֲדָשׁוֹת + להזמין + הודעות + אין חיבור לאינטרנט + כדי לצפות במכשירים המחוברים, עליך להיות מחובר לביומטריה. + המרשם נמחק ב- %s + נמחק + אין מתכונים + אין לך מתכונים בארכיון. + קבלות ההוצאות שלך נמחקו + קבלת עלות + נדרשת הרשמה + אנא היכנס כדי להציג מכשירים מחוברים. + לִרְשׁוֹם + לִרְשׁוֹם + התחבר לביטוח %s + כרטיס בריאות בסמארטפון + לא מחובר + רָשׁוּם + לְרַעֲנֵן + תקבל יומני גישה אם אתה מחובר לשירות המרשם. + נדרשת הרשמה + אנא התחבר עם כרטיס הבריאות שלך ושמור את פרטי הכניסה שלך עם ביומטריה כדי לראות את המכשירים המחוברים. + %1$s x בוקר + %1$s x ארוחת צהריים + %1$s x ערב + %1$s x בלילה + אלא אם כן הרופא שלך נתן לך הוראות שונות, ניתן להבין את הוראות השימוש כדלקמן: + הרופא שלך נתן לך מידע זה לגבי נטילת התרופה. + אין מידע על אופן נטילת התרופה במרשם שלך. + הרופא שלך ציין שקיבלת הנחיות כיצד לקחת תרופה זו שאינה במרשם. זה יכול להיות בתוכנית התרופות שלך, למשל. + לא מוגדר + תַקלִיטָן + הוראות שימוש + " %s - %s" + תזכורת להכנסות + קח תזכורות + תרופות דרך הפה + לגבי תזכורות הצריכה + שַׁחַר + צָהֳרַיִים + צָהֳרַיִים + בערב + תְרוּפָה + מידע על מינון + 1 + %s / %s + מנה אחת + שנה מינון + לְבַטֵל + תזכורת לטיפול תרופתי + לוח זמנים לתרופות + קח את המינון לפי מרשם הרופא + בשעה %s קח %s x + בחר תאריכים + לְקַדֵם + לְבַטֵל + א + מִתוֹך + מַעֲקָב + בִּלתִי מוּגבָּל + בנפרד + יום ראשון + יום אחרון + הוסף זמן + אין תזכורות לצריכה + אתה יכול להגדיר תזכורות עבור המרשמים שלך + פועל + כבוי + תזכור אותי + כבה את אופטימיזציית הסוללה עבור אפליקציה זו. + אם לא תפעיל את האפשרות הזו, תזכורות לתרופות עלולות להיראות לא אמינות. + לְבַטֵל + לְהַתִיר + הוראות שימוש + לא מוגדר + מֵידָע + זכור גם במצב אופטימיזציה של הסוללה + שנה מינון + קָהָל + טוֹפֶס + הסתיים + בִּלתִי מוּגבָּל + אל %s + לַחֲזוֹר עַל %s + במידת האפשר, אנו משתמשים במידע המאוחסן במרשם לצורך החישוב. + לְבַטֵל + לְהַצִיל + עד סוף החבילה + זְמַן + מָנָה + צפה בתרופות + האם כבר לקחת את התרופות שלך? + קח תזכורות + אין זיכרונות פעילים + אין לך תזכורות לקחת היום + בחר תאריך התחלה + בחר תאריך סיום + קַבָּלַת פָּנִים + באפליקציית המרשם האלקטרוני שלך + נסה שוב + הזן סיסמה + אנא ודא שלכל מי שאתה יכול לשתף את המכשיר הזה ושיודע את פרטי ההתחברות שלך יש גם גישה למתכונים שלך. + הזן סיסמה + אנא הזן את הסיסמה כדי לבטל את נעילת האפליקציה. + סִיסמָה + סִיסמָה + נא להזין סיסמה כדי לאבטח את האפליקציה. + הצג סיסמה + חזור על הסיסמה + שכחת את הסיסמה שלך? נא למחוק את האפליקציה ולאחר מכן להתקין אותה מחדש. אתה יכול לגלות מדוע ב %s שלנו. + הזן סיסמה + הסיסמה חייבת להיות באורך שמונה תווים לפחות + חוזק הסיסמה אינו מספיק + חוזק הסיסמה מספיק + גיבוי המכשיר אינו אפשרי + אנא הגדר גיבוי מכשיר תחילה. + לְבַטֵל + הגדרות + חוזק הסיסמא טוב מאוד + אבטחת אפליקציה + גיבוי המכשיר + בחר לפחות שיטה אחת לגיבוי האפליקציה. + סִיסמָה + שנה סיסמה + כדי למחוק, יש ליצור חיבור לשרת המרשם + הסחורה מוכנה + הסחורה מוכנה מאז %s + הסחורה הייתה מוכנה זה עתה + הסחורה מוכנה במשך %s דקות + הסחורה מוכנה מאז %s + מושבת כי לא הוזנה סיסמה. + מושבת מכיוון שהסיסמה חלשה מדי. + מושבת מכיוון שהסיסמאות אינן תואמות. + לַחקוֹר + מרשם תרומת איברים + פנקס תרומת איברים פתוח? + אתה תופנה לפנקס תרומת איברים. על מנת לצפות ולשנות את נתוני תרומת האיברים שלך, עליך להתחבר לשם. + לִפְתוֹחַ + לְבַטֵל + הפונקציה לא פעילה במצב הדגמה + diff --git a/app/features/src/main/res/values-nl/strings.xml b/app/features/src/main/res/values-nl/strings.xml index 64f9f3b8..0bac41e3 100644 --- a/app/features/src/main/res/values-nl/strings.xml +++ b/app/features/src/main/res/values-nl/strings.xml @@ -26,7 +26,7 @@ Annuleer niet Laten we gaan Wat je nodig hebt: - Voer het kaarttoegangsnummer in + Toegangsnummer invoeren voer de pincode in Probeer het nog eens Kan geen verbinding maken met de server. @@ -55,26 +55,23 @@ afdruk editor gematik GmbH\n Friedrichstraße 136\n 10117 Berlijn - Directeur: Dr. Florian Hartge\n Registratierechtbank: arrondissementsrechtbank Berlijn-Charlottenburg\n KvK-nummer: HRB 96351\n Btw-identificatienummer: DE241843684 + Beheer: dr. Florian Fuhrmann, Brenya Adjei, dr. Florian Hartge\nRegistratierechtbank: arrondissementsrechtbank Berlijn-Charlottenburg\nHandelsregisternummer: HRB 96351\nBTW-identificatienummer: DE241843684 Verantwoordelijk voor de inhoud - dr. Florian Hartge + dr. Florian Fuhrmann, Brenya Adjei, dr. Florian Hartge Contact Kennisgeving We streven ernaar om gendergelijke taal te gebruiken. Mocht u fouten tegenkomen, dan horen wij dat graag per e-mail. Het moderne platform van Duitsland voor digitale geneeskunde Email schrijven Website openen - Welkom - Registratie starten Ontgrendelen Register Annuleren - Beveiliging Legaal afdruk gegevensbescherming Gebruiksvoorwaarden - Details + Receptdetails Markeer als ingewisseld Markeer als niet-ingewisseld Doseringsvorm @@ -96,8 +93,6 @@ Installatienummer Telefoon nummer E-mailadres - Arbeidsongeval - Dag van het ongeval Ongevallenbedrijfs- of werkgeversnummer Wil je dit recept definitief verwijderen? Verwijderen @@ -120,7 +115,6 @@ Open scanner voor recepten Instellingen Schermafbeeldingen onderdrukken - Voorkomt dat er een voorbeeldafbeelding wordt weergegeven bij het schakelen tussen apps Staat u toe dat E-Prescription uw gebruiksgedrag anoniem analyseert? Technische informatie Beveiliging van uw receptgegevens @@ -142,23 +136,15 @@ Anonieme analyse blijft uitgeschakeld %s Bedankt voor uw steun! Register - Identificeer uzelf om recepten te downloaden. - Opmerking voor apotheken: De contactgegevens en informatie over apotheken verkrijgen wij van mein-apothekenportal.de van de Duitse Apothekenvereniging Heeft u een fout ontdekt of wilt u gegevens corrigeren? + Identificeer uzelf alstublieft. + Opmerking voor apotheken: De contactgegevens en informatie over apotheken verkrijgen wij van mein-apothekenportal.de. Heeft u een fout ontdekt of wilt u gegevens corrigeren? Kom meer te weten Apotheken Helaas werkte dat niet \uD83D\uDE15 Probeer het opnieuw. - Voer wachtwoord in - Verder Toegankelijkheid - Zoom - Hiermee kunt u de app vergroten door te knijpen om te zoomen. - wachtwoord - Beveilig uw gegevens met een wachtwoord naar keuze. - wachtwoord + Schakel zoomen in Redden - Laat wachtwoord zien - herhaal wachtwoord Aanbevelingen: %s Email schrijven Wanneer u uw bericht verzendt, wordt de volgende informatie over de gebruikte hardware en het besturingssysteem verzonden: @@ -169,7 +155,7 @@ Verzending filter Filter - Geen locatie beschikbaar + Deel locatie Begrepen Herhaalde wachtwoordovereenkomsten Fout 20 10 76631 @@ -179,8 +165,6 @@ Er zijn %s mislukte inlogpogingen gedetecteerd. Er zijn %s mislukte inlogpogingen gedetecteerd. - Kies de beste apparaatback-up - Dit kan een vingerafdruk, veegpatroon of iets dergelijks zijn Munten Toegangstokens SSO-tokens @@ -188,7 +172,7 @@ geen SSO-token beschikbaar naar het klembord gekopieerd Klik om het token naar het klembord te kopiëren - Alleen vandaag geldig + Alleen vandaag inwisselbaar Toestaan geen verbinding met de server Probeer het over een paar minuten opnieuw @@ -254,7 +238,7 @@ %s nieuwe recepten Inwisselbaar - In verlossing + Wordt verwerkt Ingewisseld Onbekend Toegangslogboeken bekijken @@ -266,7 +250,7 @@ Het recept is momenteel in bewerking en kan niet worden verwijderd Aanvaarden Blijkbaar werkte dat niet - Wij zijn ons ervan bewust dat de verbinding met de zorgkaart valkuilen kent. Inschrijven moet in de toekomst ook mogelijk zijn via een reeds geauthenticeerde zorgverzekeringsapp. \n\n Ook werken we eraan dat recepten digitaal kunnen worden ingewisseld zonder registratie. \n\n Is u tijdens dit proces iets opgevallen dat u graag met ons wilt delen? Schrijf ons gerust, wij ontvangen ook graag zeer kritische feedback. + Wij zijn ons ervan bewust dat de verbinding met de zorgkaart valkuilen kent. Inschrijven moet in de toekomst ook mogelijk zijn via een reeds geauthenticeerde zorgverzekeringsapp.\n\nWe werken er ook aan om ervoor te zorgen dat recepten digitaal kunnen worden ingewisseld zonder registratie.\n\nIs u tijdens dit proces iets opgevallen dat u met ons wilt delen? Schrijf ons gerust, wij ontvangen ook graag zeer kritische feedback. Verbindingstips Verbeter de sterkte van de verbinding Verwijder indien nodig de beschermhoes. @@ -289,7 +273,7 @@ Gescand op %s Gemarkeerd als ingewisseld op %s Hoe wil je verder? - Volgorde + Naar apotheek sturen Binnenkort beschikbaar Reserveer nu voor afhaling of laat het bezorgen via koerier of verzending Bewaar voor latere bestelling @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Gegevensbescherming en gebruik Verder - De pincode voor uw zorgpas heeft u van uw zorgverzekeraar ontvangen via een beveiligde procedure zoals Postident. + U moest de pincode van uw zorgkaart actief bij uw zorgverzekeraar aanvragen en deze vervolgens via een beveiligd proces zoals Postident ontvangen. Geen pincode ontvangen pincode Controleer de internetverbinding en de tijd-/datuminstellingen van uw apparaat. - Om in te loggen, drukt u op “Ontgrendelen”. Buitengesloten? Controleer uw biometrische gegevens op dit apparaat. - Wachtwoord vergeten? Verwijder de app en installeer deze vervolgens opnieuw. Je kunt in onze %s ontdekken waarom. hulp gebied Verpakkingsgrootte en eenheid actief ingrediënt @@ -338,10 +320,6 @@ Ongedaan gemaakt Kennisgeving Help ons deze app beter te maken - Voer wachtwoord in - Het wachtwoord moet minimaal acht tekens lang zijn - Wachtwoordsterkte niet voldoende - Wachtwoordsterkte voldoende Wachtwoord is zichtbaar Wachtwoord is niet zichtbaar biometrie @@ -446,9 +424,6 @@ Zojuist verzonden Verzonden om %s tijd Niet meer geldig - Registreren met app - Kies een verzekering - Niet gevonden wat je zocht? Deze lijst wordt voortdurend uitgebreid. Registratie met een zorgkaart wordt al door elke zorgverzekeraar ondersteund. Feedback van de e-receptenapp Wij kijken uit naar uw feedback. Gebruik de volgende ruimte en wees zo nauwkeurig mogelijk: PUK @@ -468,7 +443,7 @@ Kennisgeving Aanvaarden Beveiliging van uw receptgegevens - \"Deze app maakt gebruik van de veiligste biometrische sensor die door uw apparaat wordt geleverd om uw inloggegevens te beveiligen in een beschermd gebied van de apparaatopslag.\" + \Deze app maakt gebruik van de veiligste biometrische sensor die door uw apparaat wordt geleverd om uw inloggegevens te beveiligen in een beschermd gebied van de apparaatopslag.\ Door de biometrische beveiliging van uw toegangsgegevens kunt u deze app in de toekomst openen, recepten bekijken, opvragen, inwisselen of verwijderen zonder zorgpas en het invoeren van uw pincode. Zorg ervoor dat mensen met wie u dit apparaat deelt en wier biometrische kenmerken mogelijk op dit apparaat zijn opgeslagen, ook toegang hebben tot uw recepten. dat werkte helaas niet @@ -481,7 +456,7 @@ Achternaam Verzekering Verzekerings nummer - Kaarttoegangsnummer + Toegangsnummer Register Uitloggen Redden @@ -497,8 +472,7 @@ verbinding verbroken Nu verbinding maken met de receptserver? Geen tokens - U ontvangt een token wanneer u bent ingelogd bij de receptenservice.\n - Bestellingen + U ontvangt een token wanneer u bent ingelogd bij de receptenservice. Selecteer de gewenste pincode Ontgrendel kaart Selecteer Pincode @@ -515,26 +489,26 @@ Ophaalcode ontvangen Bericht kan niet worden getoond Neem contact op met uw apotheek ( %s ). - Toon winkelwagenlink + Apotheek link Toon afhaalcode Laat het bericht zien %s om %s uur - Recept verzonden naar %s . Het digitale aflossingsproces is voor veel apotheken nog onbekend. Mocht u morgen nog niets gehoord hebben, dan adviseren wij u uit voorzorg even te bellen voor navraag. + Recept verzonden naar %s . Sommige apotheken beschikken nog niet over een digitale antwoordmogelijkheid. Indien er morgen geen reactie is, verzoeken wij u uit voorzorg te bellen. Besteloverzicht Nieuw Cursus De bestelling - Gratis voor de beller. Servicetijden: ma t/m vr 8.00 - 20.00 uur behalve op nationale feestdagen + Gratis voor de beller. Servicetijden: ma - vr 8.00 - 20.00 uur behalve op feestdagen Apotheek Selecteer de gewenste pincode Gewenste pincode opgeslagen Momenteel open en bij mij in de buurt Filteren op … - start met zoeken + Zoeken Directe opdracht Apotheken Telefoonnummer (optioneel) - Zoek op naam of adres + Zoeken Geen geldige apotheekinformatie Er zijn geen actuele gegevens gevonden over deze apotheek. De vermelding voor deze apotheek wordt verwijderd. OK @@ -542,7 +516,7 @@ Momenteel is er geen actuele informatie over deze apotheek beschikbaar. Controleer uw internetverbinding. Annuleren Probeer het nog eens - Milieu besparen + Bespaar milieu Inloggen is niet mogelijk Het lijkt erop dat uw biometrische inlogkenmerken zijn gewijzigd. Log opnieuw in met uw zorgpas. Annuleren @@ -554,16 +528,15 @@ Productverbeteringen Anonieme analyse Help ons deze app beter te maken. Alle gebruiksgegevens worden anoniem verzameld en uitsluitend gebruikt om de gebruikerservaring te verbeteren. - App-beveiliging persoonlijke instellingen Toegankelijkheid Productverbeteringen Recept toegevoegd Recept al beschikbaar Er is een fout opgetreden tijdens het importeren + Toegangsnummer Verwijderen Gescand recept - Vervangingsvoorbereiding mogelijk Pincode vergeten %s Recept @@ -579,7 +552,7 @@ U kunt deze beslissing op elk moment wijzigen in de systeeminstellingen. Doorgaan Aanvaarden - Deze app gebruikt de veiligste methode die door uw apparaat wordt geboden. + De app gebruikt de veiligste methode die u op uw apparaat heeft ingesteld. Redden Kiezen medicijn @@ -607,15 +580,13 @@ Wat is een directe opdracht? Bij directe verwijzing wordt een recept uit een praktijk of ziekenhuis rechtstreeks bij een apotheek afgevuld. Verzekerde personen hoeven geen actie te ondernemen en kunnen niet tussenkomen in het terugbetalingsproces. \n\n Directe verwijzingen staan ​​vermeld in de e-receptenapp om uw behandeling voor u transparanter te maken. Kosten voor noodservice - Soms is haast noodzakelijk. Sommige recepten kunnen worden ingevuld zonder extra kosten voor de spoedservice, bijvoorbeeld \'s nachts of op feestdagen. + Indien een recept tussen 20.00 uur en 06.00 uur of op zon- en feestdagen wordt afgevuld, kan er een toeslag van 2,50 euro in rekening worden gebracht. Medicijnen tegen eigen bijdrage Vrijgesteld van bijbetaling - Degenen met een wettelijke ziektekostenverzekering moeten een extra vergoeding van maximaal tien euro betalen voor voorgeschreven medicijnen. \n\n De hoogte van de bijbetaling is afhankelijk van de prijs van uw medicijnen. Geneesmiddelen die minder dan € 5,- kosten, moet u zelf betalen.\n Voor medicijnen die duurder zijn, moet je tien procent van de prijs betalen, maar minimaal € 5 en maximaal € 10. \n\n Kinderen en jongeren onder de 18 jaar zijn over het algemeen vrijgesteld van bijbetaling. \n\n Als uw jaarkosten voor medicijnen hoger zijn dan uw financiële lastengrens, kunt u worden vrijgesteld van de eigen bijdrage. Praat hierover met uw zorgverzekeraar. + Mensen met een wettelijke ziektekostenverzekering betalen doorgaans maximaal 10 euro voor voorgeschreven medicijnen. Er kunnen hogere tarieven van toepassing zijn als er een medicijn van een specifieke fabrikant wordt aangevraagd dat niet onder de kortingsovereenkomst van de zorgverzekering valt (“medicijn aanvragen”). \n\n Kinderen en jongeren onder de 18 jaar zijn vrijgesteld van aanvullende betalingen. \n\n Indien recepten later dan 28 dagen na uitgifte worden ingewisseld, moeten de kosten volledig worden gedragen. \n\n Als u in de loop van het jaar veel medicijnen uitgeeft, kunt u bij uw zorgverzekeraar ontheffing van de eigen bijdrage aanvragen U bent vrijgesteld van het betalen van een eigen bijdrage voor dit geneesmiddel. Uw zorgverzekeraar vergoedt de kosten van de medicijnen. Hoe lang is dit recept geldig? Tijdens deze periode kunt u uw recept in elke apotheek inwisselen tegen een maximale bijbetaling van € 10,-. - Vervangingsvoorbereiding mogelijk - Vanwege wettelijke eisen van uw zorgverzekeraar kan het zijn dat u een alternatief krijgt met dezelfde werkzame stof. \n\n Geneesmiddelen kunnen er anders uitzien en anders heten, verschillende prijzen en fabrikanten hebben, maar toch dezelfde werkzame stof bevatten. De werkzame stof zelf en de dosering zijn cruciaal voor de werking van medicijnen in het lichaam. Vaak krijgen patiënten bij de apotheek een ander medicijn dan de arts heeft voorgeschreven, mits de medicijnen vergelijkbaar zijn. Er kunnen therapeutische en economische redenen zijn voor de verandering. Gescand recept Recepten die van een papieren versie zijn geïmporteerd, kunnen om veiligheidsredenen geen persoonlijke of medische informatie weergeven. \n\n Log in op deze app met zorgpas of verzekeringsapp om alle informatie op het recept te bekijken. Recept onjuist @@ -625,7 +596,7 @@ telefoon website Mail - Sorteren op afstand niet mogelijk. + Deel uw locatie om apotheken bij u in de buurt te vinden. OK Voer de huidige pincode in Verkeerde pincode ingevoerd @@ -643,12 +614,10 @@ Gezondheidskaart biometrie Niet ingelogd - Wij zijn geïnteresseerd in uw mening. Neem vijf minuten de tijd om onze enquête in te vullen. Alvast heel erg bedankt. Waarschuwingsbericht Apotheek toegevoegd aan favorieten Apotheek verwijderd uit favorieten Mijn apotheken - Wachtwoordsterkte zeer goed Schrijfbewerking niet succesvol Pincode kan niet worden opgeslagen Rapport @@ -667,7 +636,6 @@ Ingewisseld op %s Zojuist verzilverd Ingewisseld om %s uur - Bestellingen Dit recept is ingevuld als onderdeel van een behandeling voor u. Kosten voor noodservice Dit recept kan niet \'s nachts in een apotheek worden afgevuld zonder extra betaling van een spoedservicetoeslag. @@ -681,7 +649,7 @@ Recepten digitaal ontvangen? Trek het scherm omlaag om te vernieuwen. Geen recepten - Meld u aan om automatisch recepten te ontvangen of voeg een nieuw recept toe met behulp van de ⊕ in de bovenhoek. + Meld u aan om automatisch recepten te ontvangen. Register Receptarchief Misschien later @@ -718,9 +686,9 @@ Klik op het scherm om de weergegeven tooltip over te slaan. Hoe inwisselen? Hoe wilt u uw medicatie ontvangen? - Direct inwisselen - Medicijnen ter plekke inwisselen - Volgorde + Toon code + Laat het scannen bij de apotheek + Naar apotheek sturen Reserveer of laat het bezorgen Klaar Ophaalcode @@ -786,7 +754,7 @@ Hiermee worden alle onkostenbewijzen van dit apparaat en de server verwijderd. Ontvang kostenbewijzen Ook uw kostenberekeningen worden op de receptenserver opgeslagen. - Ontvangen + Activeren Totaal: %s %s Kiezen Splitsen @@ -835,17 +803,17 @@ Bijbehorende pincode vereist Alleen morgen te verzilveren als zelfbetaler Nog maar %s dagen om in te wisselen als zelfbetaler - \nNog steeds inwisselbaar als zelfbetaler voor %s dagen\n - Alleen geldig voor %s dagen - \nGeldig voor nog %s dagen\n - Alleen morgen geldig + Nog steeds inwisselbaar als zelfbetaler voor %s dagen + Nog maar %s dagen om in te wisselen + Nog steeds inwisselbaar voor %s dagen + Alleen morgen inwisselbaar Kosten worden in rekening gebracht Neemt een verzekering De recept(en) zijn succesvol overgedragen. Het recept kan niet worden verwerkt. Probeer het opnieuw. Mogelijk moet u een andere apotheek kiezen. Het recept kan niet worden verwerkt. De apotheek meldt een onbekende fout. Probeer indien nodig een andere apotheek. Het recept werd door de apotheek afgewezen. Het recept is mogelijk ongeldig of uw afleveradres of contactgegevens zijn mogelijk ongeldig. - Kan niet inwisselen. Controleer uw internetverbinding. + Probeer het opnieuw en kies eventueel een andere apotheek. Als de fout zich blijft voordoen, neem dan contact op met support. Het recept is succesvol overgedragen. De apotheek meldt echter een verwerkingsfout. Neem dan contact op met de apotheek. Het recept werd door de apotheek afgewezen. Het recept is al ingewisseld. Het recept werd door de apotheek afgewezen. Het recept is verwijderd. @@ -878,7 +846,6 @@ Activeer de demomodus Niet nu Zoekopdracht - NFC-compatibele smartphone vereist Ontvang kostenberekeningen digitaal Zodra de kostenbewijzen zijn geactiveerd, vindt u deze hier na het inwisselen van uw recept. Activeren @@ -886,11 +853,11 @@ Zodra de apotheek de kostennota heeft gedeponeerd, verschijnt deze hier. Kostenontvangst Ontvang kostenberekeningen digitaal - Let op: U ontvangt uw kostenbewijzen niet meer als afdruk in de\n Apotheek.\n + Let op: U ontvangt uw kostenbewijzen niet meer als afdruk bij de apotheek. Activeren Misschien later overeenkomst - Met uw toestemming mag u het niet afdrukken in de apotheek\n en uw kostenberekeningen in de toekomst digitaal indienen.\n + Met uw toestemming print u deze niet af bij de apotheek en dient u uw kostenbewijzen in de toekomst digitaal in. Annuleren Overeengekomen Verlaat de demomodus @@ -905,17 +872,17 @@ Geen internet Er is geen verbinding met internet. Onjuist verzoek - Er is een probleem opgetreden met het verzoek. Wij werken aan een oplossing.\n + Er is een probleem opgetreden met het verzoek. Wij werken aan een oplossing. server reageert niet Probeer het over een paar minuten opnieuw. Kon niet verbinden - Er kan momenteel geen informatie worden opgehaald. Alsjeblieft\n Probeer het later nogmaals.\n + Er kan momenteel geen informatie worden opgehaald. Probeer het later opnieuw. Afgewezen - De server heeft uw verzoek afgewezen. Alstublieft probeer het\n nogmaals later.\n + De server heeft uw verzoek afgewezen. Probeer het later opnieuw. App bijwerken - Update uw app om deze functie te gebruiken.\n + Update uw app om deze functie te gebruiken. Aanmelden mislukt - De server had een probleem met inloggen. Neem alstublieft contact op\n opnieuw.\n + Er was een probleem met inloggen op de server. Meld u opnieuw aan. Register Kopie URL Maak verbinding met de externe zorgverzekeringsapp. @@ -941,12 +908,285 @@ Aanbevolen Digitale gezondheids-ID Instellingen aanpassen? - 404 pagina? Schakel het openen van links in uw instellingen in om door te gaan. Open instellingen Verouderde app-versie Uw versie van de app is verouderd en wordt niet langer ondersteund. Update de app om het e-recept te blijven gebruiken. Updaten - Uw CAN is %s cijfers lang. + Uw toegangsnummer is %s cijfers lang. Uw pincode kan %s tot %s cijfers lang zijn. + Ophalen + Koerier + Verzending + NFC-compatibele smartphone vereist + We konden het verzoek om het recept te verwijderen niet verwerken. We werken aan een oplossing ( %s ). + Er is iets misgegaan bij het verwijderen van mijn recept, ik heb de foutcode %s ontvangen. Laat het mij weten als verwijderen weer mogelijk is. + Verder + Onjuist verzoek + Rapport + Lokaal verwijderen + Geen internet + Het recept kon niet worden verwijderd. Controleer uw internetverbinding en probeer het opnieuw. + Probeer het nog eens + OK + Opnieuw inloggen + Om het recept te verwijderen (401) moet u ingelogd zijn. + Register + Annuleren + Verwijderen onmogelijk + U kunt het recept op dit moment niet verwijderen omdat het bij de apotheek ligt om te worden ingevuld of omdat het een directe opdracht betreft (403). + Te veel verzoeken + U heeft te vaak geprobeerd het recept te verwijderen. Probeer het later opnieuw (429). + Recept verwijderen + Oeps... + "Er is een onverwachte serverfout opgetreden. Probeer het later opnieuw (500)." + App-beveiliging niet mogelijk. Stel vooraf biometrische beveiliging (bijvoorbeeld vingerafdruk) in op uw apparaat. + Biometrische back-up is niet mogelijk voor dit apparaat, omdat dit niet door uw apparaat wordt ondersteund. + Let op! Je telefoon is niet goed beschermd. + Uw inloggegevens worden op uw telefoon opgeslagen. Om de toegang te beveiligen, wordt uw favoriete inlogmethode gebruikt. Deze inlogmethode, bijvoorbeeld het veegpatroon, is niet erg veilig. U gebruikt de functie ‘Inloggegevens opslaan’ op eigen risico. + Als u nog steeds gebruik maakt van de functie ‘Inloggegevens opslaan’, kunt u in de toekomst recepten bekijken, openen, inwisselen of verwijderen met de e-receptenapp zonder zorgpas en het invoeren van de pincode. + Locatie niet gevonden + Naar apotheek sturen + Toon code + Taal + Taal + Duits + Arabisch + Bulgaars + Tsjechisch + Deens + Engels + Frans + Hebreeuws + Italiaans + Nederlands + Pools + Roemeense + Russisch + Turks + Oekraïens + Standaard + Digitale kostenbewijzen zijn gedeactiveerd en verwijderd. + “E-receptenforum” + Geen vervangende bereiding mogelijk + Vervangingsvoorbereiding mogelijk + Vervangingsbereiding (Aut idem) + Apothekers zijn verplicht voorrang te geven aan de verstrekking van geneesmiddelen waarvoor de zorgverzekeraar van de patiënt een kortingsovereenkomst heeft gesloten met de geneesmiddelenfabrikanten. Dit geldt alleen niet als de arts ‘Aut idem’ op het recept uitsluit, wat bij uw recept niet het geval is. + Uw arts heeft bepaald dat u de voorgeschreven medicijnen moet krijgen. De apotheek mag geen omruiling uitvoeren op basis van een kortingsovereenkomst (“Aut idem”). + Schermafbeeldingen toestaan + Als je schermafbeeldingen toestaat, blijft de laatst geopende pagina zichtbaar op de achtergrond wanneer je van app wisselt. Indien nodig kunnen uw persoonsgegevens zichtbaar zijn. Wij raden daarom aan geen screenshots toe te staan. + Toestaan + Annuleren + Schakel je toetsenbord over naar stickers of gebruik emoji\'s voor je profielfoto. + Kies profielfoto + Hoe wil je verder? + Selecteer foto + camera + Emoji + Annuleren + Open instellingen + Gebruik + Maak een foto + Profielfoto wijzigen + %s minuten geleden aangenomen + Geaccepteerd op %s + Gewoon geaccepteerd + Geaccepteerd om %s uur + GezondheidID + Kies een verzekering + Als het registreren met de Health ID niet verloopt zoals verwacht, volg dan de tips van ons helpcentrum. + Hulp + Hulp + Tips voor het aanmelden bij de verzekeringenapp + Uw verzekeringsmaatschappij is verantwoordelijk voor de Health ID. Als u vragen heeft over de registratie, kunt u contact met hen opnemen. Hier zijn enkele beproefde tips: + Houd er rekening mee dat, afhankelijk van uw verzekering, een aparte app nodig kan zijn. Vraag bij uw verzekeraar na wat dit is. + Start de kassa-app en log daar eenmalig in voordat u begint met registreren in de e-recepten-app. + Het wisselen tussen aanmelden met de HealthID en de zorgkaart kan tot problemen leiden. Meld u daarom eerst actief af bij uw profiel voordat u de inlogmogelijkheid wijzigt. + Staat uw verzekering niet op de lijst, dan kunt u zich ook aanmelden met uw zorgpas en de bijbehorende pincode. + Indien de kassa-app u niet terugstuurt naar de e-recepten-app, meld deze fout dan zeker aan uw verzekeringsmaatschappij. + Als de verzekeringsapp niet toegankelijk is, kan het handig zijn om naar de browserinstellingen van uw smartphone te gaan en het openen van links toe te staan. + Als uw smartphone Android 14 gebruikt, kan het handig zijn om het openen van links in de instellingen toe te staan. + Open instellingen + dat werkte helaas niet + Probeer het later opnieuw. + Foutmeldingen bij het laden. + Kies een emoji of tekst + Redden + Log in om door te gaan + Verder + Telefoongesprek + Schrijven\nMail + Naar de apotheek + Apotheek + Vandaag + Morgen + Route\nhier + Voorschrift en kostenbon verwijderen + Als u het recept verwijdert, wordt ook de bijbehorende kostenbon verwijderd. + Berichten van alle profielen worden nu samen weergegeven + Nieuwe functies + Nieuwe functies + We werken voortdurend aan nieuwe functies. Deel uw mening met ons en help ons een betere app te bouwen. + Bestellingen voor iedereen + Onder Bestellingen kun je nu de bestellingen voor alle profielen bij elkaar zien en veel gemakkelijker bekijken + + Geen vergoeding van kosten + De zorgverzekering vergoedt in de regel niet de kosten voor dit recept. U bent als patiënt dus zelf verantwoordelijk voor de volledige betaling. Of de kosten vergoed worden vanuit een aanvullende verzekering of wettelijke uitkering moet individueel worden gecontroleerd. + Uw verzekering vergoedt geen kosten voor dit recept. + + "Uw verzekering dekt het recept %s niet" + "Uw verzekering dekt de recepten %s niet." + + Veroorzaakt + Datum + Ongeluk + Arbeidsongeval + Industriële ziekte + Nieuws + Geen nieuws + Je hebt nog geen berichten. + 🎉Je bestelling staat klaar om afgehaald te worden. Ter identificatie dient u deze afhaalcode te tonen. + Helaas was het bericht van uw apotheek leeg. Neem dan contact op met uw apotheek. + De apotheek heeft u een link verstrekt. + Nieuws + Volgorde + Berichten + Geen internetverbinding + Om de aangesloten apparaten te bekijken, moet u verbonden zijn met de biometrie. + Recept is verwijderd op %s + Verwijderd + Geen recepten + Je hebt geen recepten in het archief. + Uw onkostenbewijzen zijn verwijderd + Kostenontvangst + Registratie vereist + Log in om aangesloten apparaten te bekijken. + Register + Register + Maak verbinding met %s Verzekeringen + Gezondheidskaart op smartphone + Niet ingelogd + Geregistreerd + vernieuwen + U ontvangt toegangslogboeken als u bent ingelogd bij de receptenservice. + Registratie vereist + Log in met uw zorgkaart en sla uw inloggegevens met biometrie op om de aangesloten apparaten te bekijken. + %1$s x ochtend + %1$s x Lunch + %1$s x avond + %1$s x \'s Nachts + Tenzij uw arts u andere instructies heeft gegeven, kunnen de gebruiksaanwijzingen als volgt worden begrepen: + Uw arts heeft u deze informatie gegeven over het innemen van de medicatie. + Op uw recept staat niet hoe u de medicatie moet innemen. + Uw arts heeft opgemerkt dat u instructies heeft gekregen over het gebruik van dit medicijn, die niet op het recept staan. Dit kan bijvoorbeeld op uw medicatieplan staan. + Niet gespecificeerd + DJ + Instructies voor gebruik + " %s - %s" + Herinnering aan inkomsten + Neem herinneringen op + Orale medicijnen + Over de intakeherinneringen + Ochtend + Middag + Middag + \'s Avonds + medicijn + Doseringsinformatie + 1 + %s / %s + 1 dosis + Dosering wijzigen + Annuleren + Medicatieherinnering + Medicatie schema + Neem de dosering volgens doktersvoorschrift + Om %s uur neem je %s x + Selecteer data + Verder + Annuleren + A + Uit + Volgen + Onbeperkt + Individueel + Eerste dag + Laatste dag + Voeg tijd toe + Geen intakeherinneringen + U kunt herinneringen instellen voor uw recepten + Aan + Uit + Onthoud mij + Schakel batterijoptimalisatie uit voor deze app. + Als u deze optie niet activeert, kunnen medicatieherinneringen onbetrouwbaar lijken. + Annuleren + Toestaan + Instructies voor gebruik + Niet gespecificeerd + informatie + Denk er ook aan in de batterijoptimalisatiemodus + Dosering wijzigen + Menigte + formulier + eindigde + onbeperkt + naar %s + Herhalen %s + Voor de berekening gebruiken wij waar mogelijk de gegevens die in het recept zijn opgeslagen. + Annuleren + Redden + Tot het einde van het pakket + Tijd + dosis + Bekijk medicijnen + Heeft u uw medicatie al ingenomen? + Neem herinneringen op + Geen actieve herinneringen + Je hebt vandaag geen herinneringen voor je + Selecteer startdatum + Selecteer einddatum + Welkom + in uw e-receptenapp + Probeer het opnieuw + Voer wachtwoord in + Zorg ervoor dat iedereen met wie u dit apparaat deelt en die uw inloggegevens kent, ook toegang heeft tot uw recepten. + Voer wachtwoord in + Voer het wachtwoord in om de app te ontgrendelen. + wachtwoord + Wachtwoord + Voer een wachtwoord in om de app te beveiligen. + Wachtwoord tonen + Herhaal wachtwoord + Wachtwoord vergeten? Verwijder de app en installeer deze vervolgens opnieuw. Je kunt ontdekken waarom in onze %s. + Voer wachtwoord in + Het wachtwoord moet minimaal acht tekens lang zijn + Wachtwoordsterkte niet voldoende + Wachtwoordsterkte voldoende + Apparaatback-up niet mogelijk + Stel eerst een apparaatback-up in. + Annuleren + Instellingen + Wachtwoordsterkte zeer goed + App-beveiliging + Back-up van apparaat + Kies ten minste één methode om een ​​back-up van de app te maken. + wachtwoord + Wachtwoord wijzigen + Om te verwijderen moet er een verbinding met de Receptserver tot stand worden gebracht + Goederen zijn klaar + Goederen zijn gereed sinds %s + De goederen zijn zojuist klaargemaakt + Goederen zijn al %s minuten gereed + Goederen zijn gereed sinds %s + Uitgeschakeld omdat er geen wachtwoord is ingevoerd. + Uitgeschakeld omdat het wachtwoord te zwak is. + Uitgeschakeld omdat de wachtwoorden niet overeenkomen. + Ontdekken + Register voor orgaandonatie + Register orgaandonatie openen? + U wordt doorgestuurd naar het orgaandonorregister. Om uw orgaandonatiegegevens te bekijken en te wijzigen, moet u daar inloggen. + Open + Annuleren + Functie niet actief in demomodus diff --git a/app/features/src/main/res/values-pl/strings.xml b/app/features/src/main/res/values-pl/strings.xml index 137a986b..41e4e617 100644 --- a/app/features/src/main/res/values-pl/strings.xml +++ b/app/features/src/main/res/values-pl/strings.xml @@ -28,7 +28,7 @@ Nie anuluj Chodźmy Co jest potrzebne: - Wprowadź numer dostępu do karty + Wprowadź numer dostępu Wprowadź PIN Spróbuj ponownie Nie udało się utworzyć połączenia z serwerem. @@ -59,26 +59,23 @@ Stopka redakcyjna Wydawca gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Dyrektor zarządzający: dr hab. Florian Hartge\n Sąd rejestrowy: Sąd Rejonowy Berlin-Charlottenburg\n Numer rejestru handlowego: HRB 96351\n Numer identyfikacyjny VAT: DE241843684 + Zarządzanie: dr. Floriana Fuhrmanna, Brenyi Adjei, dr. Florian Hartge\nSąd rejestrowy: Sąd Rejonowy Berlin-Charlottenburg\nNumer rejestru handlowego: HRB 96351\nNumer identyfikacyjny VAT: DE241843684 Osoba odpowiedzialna za treść - Dr. Florian Hartge + dr. Floriana Fuhrmanna, Brenyi Adjei, dr. Floriana Hartge\'a Kontakt Wskazówka Staramy się używać sformułowań neutralnych płciowo. W razie zauważenia błędów prosimy o informację drogą mailową. Nowoczesna platforma medycyny elektronicznej w Niemczech Napisz wiadomość e-mail Otwórz stronę internetową - Witamy - Rozpocznij logowanie Odblokuj Rejestr Anuluj - Bezpieczeństwo Nota prawna Stopka redakcyjna Ochrona danych Warunki korzystania - Szczegóły + Szczegóły przepisu Zaznacz jako zrealizowaną Zaznacz jako niezrealizowaną Forma dawkowania @@ -100,8 +97,6 @@ Numer zakładu pracy Numer telefonu Adres e-mail - Wypadek przy pracy - Data wypadku Numer przedsiębiorstwa, w którym nastąpił wypadek lub numer pracodawcy Czy chcesz nieodwołalnie usunąć tę receptę? Usuń @@ -124,7 +119,6 @@ Otwórz skaner recept Ustawienia Blokuj tworzenie zrzutów ekranu - Zapobiega wyświetlaniu podglądu przy zmianie aplikacji Czy pozwalasz, aby E-Recepta analizowała Twoje zachowania w sposób anonimowy? Informacje techniczne Bezpieczeństwo danych Twojej recepty @@ -146,23 +140,15 @@ Anonimowa analiza pozostaje nieaktywna %s Dziękujemy za Twoje wsparcie! Rejestr - Przeprowadź identyfikację, aby pobrać recepty. - Wskazówka dla aptek:dane kontaktowe i informacje o aptekach pozyskujemy ze strony mein-apotkekenportal.de związku Deutscher Apothekenverband e.V. Znalazłeś(-aś) błąd lub chcesz skorygować dane? + Proszę o identyfikację. + Uwaga dla aptek: Dane kontaktowe i informacje o aptekach pozyskujemy ze strony mein-apothekenportal.de. Zauważyłeś błąd lub chcesz poprawić dane? Dowiedz się więcej - apteki + Apteki Niestety nie udało się \uD83D\uDE15 Spróbuj ponownie. - Wprowadź hasło - Dalej Pomoc w obsłudze - Powiększ - Umożliwia powiększenie aplikacji poprzez rozsuwanie lub zsuwanie palców (pinch-to-zoom). - Hasło - Zabezpiecz swoje dane indywidualnym hasłem. - Hasło + Włącz powiększanie Zapisz - Wyświetl hasło - Powtórz hasło Zalecenia: %s Napisz wiadomość e-mail Podczas wysyłania wiadomości przekazywane są następujące informacje na temat używanego sprzętu i systemu operacyjnego: @@ -173,7 +159,7 @@ Wysyłka Filtr Filtruj - Brak dostępnych lokalizacji + Udostępnij lokalizację Rozumiem Ponownie wprowadzone hasło zgadza się Błąd 20 10 76631 @@ -185,8 +171,6 @@ Wykryto %s nieudanych prób logowania. - Wybierz najlepsze zabezpieczenie urządzenia - Takim zabezpieczeniem może być odcisk palca, wzór odblokowania itp. Tokeny Token dostępu Token SSO @@ -194,7 +178,7 @@ brak dostępnych tokenów SSP skopiowano do schowka Kliknij, aby skopiować token do schowka - Obowiązuje tylko dzisiaj + Możliwość wykupienia tylko dzisiaj Zezwól Brak połączenia z serwerem Spróbuj ponownie za kilka minut @@ -262,7 +246,7 @@ %s nowych przepisów Gotowe do odbioru - W odkupieniu + Jest przetwarzane Zrealizowane Nieznane Wyświetl protokoły dostępu @@ -274,7 +258,7 @@ Recepta jest teraz edytowana i nie można jej usunąć Zaakceptować Wygląda na to, że operacja nie powiodła się - Jesteśmy świadomi, że połączenie z kartą zdrowia ma swoje pułapki. W przyszłości rejestracja powinna być możliwa również za pośrednictwem już uwierzytelnionej aplikacji ubezpieczenia zdrowotnego. \n\n Pracujemy również nad umożliwieniem realizacji recept cyfrowo bez konieczności rejestracji. \n\n Czy podczas tego procesu zauważyłeś coś, czym chciałbyś się z nami podzielić? Napisz do nas, chętnie otrzymamy również bardzo krytyczne uwagi. + Jesteśmy świadomi, że połączenie z kartą zdrowia ma swoje pułapki. W przyszłości rejestracja powinna być możliwa również za pośrednictwem już uwierzytelnionej aplikacji ubezpieczenia zdrowotnego.\n\nPracujemy również nad umożliwieniem cyfrowej realizacji recept bez konieczności rejestracji.\n\nCzy podczas tego procesu zauważyłeś coś, czym chciałbyś się z nami podzielić? Napisz do nas, chętnie otrzymamy również bardzo krytyczne uwagi. Porady dotyczące połączenia Zwiększ siłę połączenia Rozwiązaniem może być usunięcie osłonki. @@ -297,7 +281,7 @@ Zeskanowano dnia %s Zaznaczono jako zrealizowaną w dniu %s Jak chcesz kontynuować? - Zamów + Wyślij do apteki Dostępne wkrótce Zarezerwuj teraz do odbioru lub zleć dostawę kurierem lub wysyłką Zapisz na potrzeby kolejnych zamówień @@ -329,13 +313,11 @@ https://www.openstreetmap.org/copyright Prywatność i użytkowanie Dalej - Otrzymałeś PIN do swojej karty zdrowia od swojego towarzystwa ubezpieczeniowego, korzystając z bezpiecznej procedury, takiej jak Postident. + Musiałeś aktywnie zamówić PIN do karty zdrowia w swoim ubezpieczycielu, a następnie otrzymać go za pośrednictwem bezpiecznego procesu, takiego jak Postident. Nie otrzymałem(am) kodu PIN PIN Sprawdź połączenie z internetem oraz ustawienie godziny/daty na swoim urządzeniu. - Aby się zalogować, naciśnij „Odblokuj”. Nie możesz uzyskać dostępu? Sprawdź swoje biometryczne dane dostępowe na tym urządzeniu. - Nie pamiętasz hasła? Usuń aplikację i następnie zainstaluj ją ponownie. Z naszego %s dowiesz się, dlaczego tak się dzieje. Pomoc Wielkość opakowania i jednostka Substancja czynna @@ -348,10 +330,6 @@ Cofnij Wskazówka Pomóż nam ulepszyć tę aplikację - Wprowadź hasło - Hasło musi zawierać co najmniej osiem znaków - Niewystarczająca siła hasła - Wystarczająca siła hasła Hasło jest widoczne Hasło jest niewidoczne Biometria @@ -458,9 +436,6 @@ Właśnie wysłano Wysłano o godzinie %s Ważność upłynęła - Zaloguj się za pomocą aplikacji - Wybierz ubezpieczenie - Nie znalazłeś tego, czego szukałeś? Lista ta jest stale poszerzana. Rejestrację z kartą zdrowia obsługuje już każda kasa chorych. Informacje zwrotne z aplikacji E-recepta Czekamy na Twój feedback. Postaraj się sformułować swoje opinie możliwie precyzyjnie i zapisz je poniżej: PUK @@ -474,13 +449,13 @@ Wskazówka Ze względów bezpieczeństwa połączenie z serwerem receptur zostaje zakończone po 12 godzinach. Aby ponownie nawiązać połączenie, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. Ustaw zabezpieczenie biometryczne - Nie można zapisać danych dostępowych. Wcześniej utwórz zabezpieczenie biometryczne (np. odcisk palca) na swoim urządzeniu. + Nie ma możliwości zapisania danych dostępowych. Wcześniej skonfiguruj zabezpieczenia biometryczne (np. odcisk palca) na swoim urządzeniu. Anuluj Ustawienia Wskazówka Zaakceptować Bezpieczeństwo danych Twojej recepty - \"Ta aplikacja używa najbezpieczniejszego czujnika biometrycznego, jaki udostępnia Twoje urządzenie, aby zabezpieczyć Twoje dane dostępowe w chronionym obszarze pamięci urządzenia.\" + \Ta aplikacja wykorzystuje najbezpieczniejszy czujnik biometryczny udostępniany przez Twoje urządzenie, aby zabezpieczyć Twoje dane uwierzytelniające w chronionym obszarze pamięci urządzenia.\ Biometryczne zabezpieczenie Twoich danych dostępowych umożliwia otwieranie tej aplikacji w przyszłości bez karty zdrowia i podawania kodu PIN, a także przeglądanie, wywoływanie, realizowanie lub kasowanie recept. Pamiętaj, że osoby, które oprócz Ciebie korzystają z Tego urządzenia i których cechy biometryczne mogą być zapisane w tym urządzeniu, także uzyskają dostęp do Twoich recept. Niestety nie udało się @@ -493,7 +468,7 @@ Imię i nazwisko Ubezpieczenie Numer ubezpieczonego - Numer dostępu do karty + Numer dostępu Rejestr Wyloguj Zapisz @@ -509,8 +484,7 @@ utracono połączenie Połączyć się teraz z serwerem przepisów? Brak tokenów - Token otrzymasz po zalogowaniu się do serwisu recept.\n - Zamówienia + Otrzymasz token, jeśli jesteś zalogowany w aplikacji E-recepta. Wybierz żądany kod PIN odblokować kartę Wybierz PIN @@ -527,26 +501,26 @@ Otrzymano kod odbioru Wiadomość nie może zostać wyświetlona Proszę skontaktować się ze swoją apteką ( %s ). - Pokaż link do koszyka + Link do apteki Wyświetl kod odbioru Pokaż wiadomość %s o godzinie %s - Przepis wysłany do %s . Dla wielu aptek proces cyfrowej realizacji zamówienia jest wciąż nieznany. Jeśli nie otrzymasz odpowiedzi do jutra, profilaktycznie zalecamy zadzwonić i zapytać. + Przepis wysłany do %s . Niektóre apteki nie mają jeszcze opcji cyfrowej odpowiedzi. Jeżeli do jutra nie będzie odpowiedzi, proszę profilaktycznie zadzwonić. Przegląd zamówienia Nowy Kurs Kolejność - Bezpłatnie dla dzwoniącego. Godziny świadczenia usług: Pon. - Pt. 8:00 - 20:00 z wyjątkiem świąt państwowych + Bezpłatnie dla dzwoniącego. Godziny świadczenia usług: poniedziałek - piątek 8:00 - 20:00 z wyjątkiem świąt federalnych Apteka Wybierz żądany kod PIN Zapisano żądany kod PIN Obecnie otwarte i blisko mnie Filtruj według … - Zacznij szukać + Szukać Przypisanie bezpośrednie apteki Numer telefonu (opcjonalnie) - Szukaj według nazwy lub adresu + Szukać Brak aktualnych informacji o aptece Nie znaleziono aktualnych informacji o tej aptece. Wpis dotyczący tej apteki zostanie usunięty. OK @@ -566,16 +540,15 @@ Ulepszenia produktu Anonimowa analiza Pomóż nam ulepszyć tę aplikację. Wszystkie dane użytkowników są gromadzone anonimowo i służą wyłącznie do optymalizacji korzystania z aplikacji. - Bezpieczeństwo aplikacji ustawienia osobiste Pomoc w obsłudze Ulepszenia produktu Dodano przepis Przepis już dostępny Wystąpił błąd podczas importowania + Numer dostępu Usuń Zeskanowana recepta - Możliwość wyboru preparatu zastępczego Zapomniany PIN %s Przepis @@ -593,7 +566,7 @@ Możesz w każdej chwili zmienić tę decyzję w ustawieniach systemowych. Kontynuuj Zaakceptować - Ta aplikacja wykorzystuje najbezpieczniejszą metodę udostępnianą przez Twoje urządzenie. + Aplikacja korzysta z najbezpieczniejszej metody, którą skonfigurowałeś na swoim urządzeniu. Zapisz Wybierać Lek @@ -621,15 +594,13 @@ Co to jest zlecenie bezpośrednie? W przypadku bezpośredniego skierowania recepta z przychodni lub szpitala realizowana jest bezpośrednio w aptece. Osoby ubezpieczone nie muszą podejmować żadnych działań i nie mogą ingerować w proces umorzenia. \n\n Bezpośrednie skierowania są wymienione w aplikacji do e-recepty, dzięki czemu leczenie jest dla Ciebie bardziej przejrzyste. Opłata za obsługę w nagłych wypadkach - Czasami zachodzi potrzeba pośpiechu. Niektóre recepty można zrealizować bez dodatkowej opłaty za pogotowie, na przykład w nocy lub w dni wolne od pracy. + W przypadku realizacji recepty w godzinach od 20:00 do 6:00 lub w niedziele i święta może zostać pobrana dodatkowa opłata w wysokości 2,50 euro. Leki objęte współpłatnością Zwolnione z dodatkowej opłaty - Osoby posiadające ustawowe ubezpieczenie zdrowotne muszą uiścić dodatkową opłatę w wysokości do dziesięciu euro za leki na receptę. \n\n Wysokość dopłaty uzależniona jest od ceny leku. Za leki, które kosztują mniej niż 5 euro, musisz sam zapłacić.\n Za droższe leki trzeba zapłacić dziesięć procent ceny, ale co najmniej 5 euro, a maksymalnie 10 euro. \n\n Dzieci i młodzież do lat 18 są co do zasady zwolnione z dodatkowej opłaty. \n\n Jeśli Twoje roczne koszty leków przekraczają Twój limit obciążeń finansowych, możesz zostać zwolniony ze współpłacenia. Porozmawiaj o tym ze swoją firmą ubezpieczeniową. + Osoby posiadające ustawowe ubezpieczenie zdrowotne płacą zazwyczaj maksymalnie 10 euro za leki na receptę. Wyższe opłaty mogą obowiązywać w przypadku zamówienia leku od konkretnego producenta, który nie jest objęty umową o zniżkę na ubezpieczenie zdrowotne („zapytaj o lek”). \n\n Z opłat dodatkowych zwolnione są dzieci i młodzież do lat 18. \n\n Jeżeli realizacja recept nastąpi później niż 28 dni od dnia ich wystawienia, koszty muszą zostać poniesione w całości. \n\n Jeśli w ciągu roku wydasz dużo leków, możesz ubiegać się o zwolnienie ze współpłacenia w swoim ubezpieczycielu Jesteś zwolniony z współpłacenia za ten lek. Koszt leku pokryje Twoja firma ubezpieczeniowa. Jak długo ważna jest ta recepta? W tym okresie możesz zrealizować receptę w dowolnej aptece za maksymalną dopłatą w wysokości 10 €. - Możliwość wyboru preparatu zastępczego - Ze względu na wymogi prawne nałożone przez firmę ubezpieczeniową możesz otrzymać lek alternatywny zawierający tę samą substancję czynną. \n\n Leki mogą wyglądać i nazywać się inaczej, mieć różnych cen i producentów, a mimo to zawierać tę samą substancję czynną. Decydujące znaczenie dla działania leków na organizm ma sam składnik aktywny oraz jego dawkowanie. Pacjenci często otrzymują w aptece inny lek niż ten przepisany przez lekarza – pod warunkiem, że lek jest porównywalny. Zmiana może mieć podłoże terapeutyczne i ekonomiczne. Zeskanowana recepta Ze względów bezpieczeństwa recepty importowane z wersji papierowej nie mogą zawierać danych osobowych ani medycznych. \n\n Zaloguj się do tej aplikacji za pomocą karty zdrowia lub aplikacji ubezpieczeniowej, aby wyświetlić wszystkie informacje zawarte na recepcie. Przepis błędny @@ -639,7 +610,7 @@ Telefon strona internetowa E-mail - Sortowanie według odległości nie jest możliwe. + Udostępnij swoją lokalizację, aby znaleźć apteki w pobliżu. OK Wpisz aktualny kod PIN Wprowadzono błędny PIN @@ -657,12 +628,10 @@ Karta ubezpieczeniowa Biometria Nie zalogowano - Jesteśmy ciekawi Twojej opinii. Prosimy o poświęcenie pięciu minut na wypełnienie naszej ankiety. Z góry bardzo dziękuję. Ostrzeżenie Apteka dodana do ulubionych Apteka usunięta z ulubionych Moje apteki - Bardzo dobra siła hasła Operacja zapisu nie powiodła się Nie można zapisać kodu PIN Zgłoś @@ -681,7 +650,6 @@ Wykorzystano w dniu %s Odkupiony właśnie teraz Wykorzystano o godzinie %s - Zamówienia Ta recepta została zrealizowana dla Ciebie w ramach leczenia. Opłata za obsługę w nagłych wypadkach Recepty tej nie można zrealizować w aptece w godzinach nocnych bez dodatkowej opłaty za pogotowie. @@ -695,7 +663,7 @@ Otrzymywać recepty cyfrowo? Pociągnij ekran w dół, aby odświeżyć. Brak recept - Zaloguj się, aby automatycznie otrzymywać przepisy lub dodaj nowy przepis za pomocą ⊕ w górnym rogu. + Zarejestruj się, aby automatycznie otrzymywać przepisy. Rejestr Archiwum przepisów Może później @@ -732,9 +700,9 @@ Kliknij ekran, aby pominąć wyświetlaną podpowiedź. Jak wykupić? W jaki sposób chcesz otrzymać leki? - Zrealizuj bezpośrednio - Zrealizuj leki na miejscu - Zamów + Pokaż kod + Zeskanuj go w aptece + Wyślij do apteki Zarezerwuj lub zleć dostawę Gotowe Kod kolekcji @@ -802,7 +770,7 @@ Spowoduje to usunięcie wszystkich rachunków za wydatki z tego urządzenia i serwera. Otrzymuj rachunki za koszty Twoje rachunki kosztów są również zapisywane na serwerze receptur. - Otrzymane + Aktywuj Razem: %s %s Wybierz Podział @@ -851,17 +819,17 @@ Wymagany powiązany kod PIN Można wykorzystać dopiero jutro jako osoba płacąca samodzielnie Pozostało tylko %s dni do wykorzystania jako osoba płacąca samodzielnie - \nNadal można go wykorzystać jako osoba samopłatna przez %s dni\n - Ważne tylko przez %s dni - \nObowiązuje przez %s dni\n - Obowiązuje tylko jutro + Nadal można go wykorzystać jako osoba samopłatna przez %s dni + Pozostało tylko %s dni na wykorzystanie + Nadal można go wykorzystać przez %s dni + Możliwość wykupienia dopiero jutro Za opłatą Bierze ubezpieczenie Przepisy zostały pomyślnie przesłane. Nie można przetworzyć przepisu. Proszę spróbuj ponownie. Być może będziesz musiał wybrać inną aptekę. Nie można przetworzyć przepisu. Apteka zgłasza nieznany błąd. Jeśli to konieczne, spróbuj innej apteki. Recepta została odrzucona przez aptekę. Recepta może być nieważna lub Twój adres dostawy lub dane kontaktowe mogą być nieprawidłowe. - Nie można zrealizować kuponu. Sprawdź swoje połączenie internetowe. + Spróbuj ponownie i ewentualnie wybierz inną aptekę. Jeśli błąd będzie się powtarzał, skontaktuj się z pomocą techniczną. Przepis został pomyślnie przesłany. Apteka zgłasza jednak błąd w przetwarzaniu. Prosimy o kontakt z apteką. Recepta została odrzucona przez aptekę. Recepta została już zrealizowana. Recepta została odrzucona przez aptekę. Przepis został usunięty. @@ -894,7 +862,6 @@ Aktywuj tryb demonstracyjny Nie teraz Szukaj - Wymagany smartfon z obsługą NFC Otrzymuj rachunki kosztów w formie cyfrowej Po aktywowaniu rachunków, znajdziesz je tutaj po zrealizowaniu recepty. Aktywuj @@ -902,11 +869,11 @@ Gdy tylko apteka zdeponuje rachunek, pojawi się on tutaj. Rachunek kosztów Otrzymuj rachunki kosztów w formie cyfrowej - Uwaga: nie będziesz już otrzymywać rachunków w postaci wydruku w formacie\n Apteka.\n + Uwaga: w aptece nie będziesz już otrzymywać rachunków w formie wydruku. Aktywuj Może później porozumienie - Za Twoją zgodą zrezygnujesz z drukowania go w aptece\n i w przyszłości przesyłaj rachunki w formie cyfrowej.\n + Za Twoją zgodą zrezygnujesz z ich drukowania w aptece i w przyszłości będziesz przesyłać rachunki w formie cyfrowej. Anuluj Wyrażam zgodę Wyjdź z trybu demonstracyjnego @@ -921,17 +888,17 @@ Brak internetu Brak połączenia z Internetem. Nieprawidłowe żądanie - Wystąpił problem z żądaniem. Pracujemy nad rozwiązaniem.\n + Wystąpił problem z żądaniem. Pracujemy nad rozwiązaniem. Serwer nie odpowiada Spróbuj ponownie za kilka minut. Nie udało się połączyć - Obecnie nie można pobrać żadnych informacji. Proszę\n Spróbuj ponownie później.\n + Obecnie nie można pobrać żadnych informacji. Spróbuj ponownie później. Odrzucony - Serwer odrzucił Twoje żądanie. Spróbuj\n Jeszcze raz później.\n + Serwer odrzucił Twoje żądanie. Spróbuj ponownie później. Zaktualizuj aplikację - Aby skorzystać z tej funkcji, zaktualizuj swoją aplikację.\n + Aby skorzystać z tej funkcji, zaktualizuj swoją aplikację. Logowanie nie powiodło się - Serwer miał problem z zalogowaniem się. Proszę, skontaktuj się\n Ponownie.\n + Serwer miał problem z zalogowaniem się. Zaloguj się ponownie. Rejestr Kopiuj URL Połącz się z zewnętrzną aplikacją ubezpieczenia zdrowotnego. @@ -957,12 +924,287 @@ Zalecana Cyfrowy identyfikator zdrowia Dostosować ustawienia? - strona 404? Aby kontynuować, włącz otwieranie linków w swoich ustawieniach. Otwórz ustawienia Nieaktualna wersja aplikacji Twoja wersja aplikacji jest przestarzała i nie jest już obsługiwana. Aby dalej korzystać z e-recepty, zaktualizuj aplikację. Aktualizuj - Długość Twojej magistrali CAN wynosi %s cyfr. + Twój numer dostępowy składa się z %s cyfr. Twój PIN może mieć długość od %s do %s cyfr. + Ulec poprawie + Kurier + Wysyłka + Wymagany smartfon z obsługą NFC + Nie mogliśmy przetworzyć żądania usunięcia przepisu. Pracujemy nad rozwiązaniem ( %s ). + Coś poszło nie tak podczas usuwania mojej recepty, otrzymałem kod błędu %s . Proszę o informację, gdy usunięcie będzie ponownie możliwe. + Dalej + Nieprawidłowe żądanie + Zgłoś + Usuń lokalnie + Brak internetu + Nie można usunąć przepisu. Sprawdź swoje połączenie internetowe i spróbuj ponownie. + Spróbuj ponownie + OK + Zaloguj się ponownie + Aby usunąć przepis musisz być zalogowany (401). + Rejestr + Anuluj + Usunięcie niemożliwe + W tym momencie nie możesz usunąć recepty, ponieważ znajduje się ona w aptece do realizacji lub jest w trakcie realizacji bezpośredniego zlecenia (403). + zbyt dużo próśb + Zbyt wiele razy próbowałeś usunąć przepis. Spróbuj ponownie później (429). + Usuń przepis + Ups... + „Wystąpił nieoczekiwany błąd serwera. Spróbuj ponownie później (500).” + Bezpieczeństwo aplikacji nie jest możliwe. Wcześniej skonfiguruj zabezpieczenia biometryczne (np. odcisk palca) na swoim urządzeniu. + Kopia zapasowa biometryczna nie jest możliwa dla tego urządzenia, ponieważ nie jest obsługiwana przez Twoje urządzenie. + Niebezpieczeństwo! Twój telefon nie jest dobrze chroniony. + Twoje dane do logowania są zapisane na Twoim telefonie. Aby chronić dostęp, używana jest preferowana przez Ciebie metoda logowania. Ta metoda logowania, np. wzór machnięcia, nie jest zbyt bezpieczna. Z funkcji „Zapisz dane do logowania” korzystasz na własne ryzyko. + Jeśli w dalszym ciągu korzystasz z funkcji „Zapisz dane do logowania”, w przyszłości będziesz mógł przeglądać, otwierać, realizować lub usuwać recepty za pomocą aplikacji e-recepty bez konieczności posiadania karty zdrowia i podawania PIN-u. + Nie znaleziono lokalizacji + Wyślij do apteki + Pokaż kod + Język + Język + Niemiecki + Arabski + Bułgarski + Czech + Duński + Język angielski + Francuski + Hebrajski + Włoski + Holenderski + Polski + Rumuński + Rosyjski + Turecki + Ukraiński + Domyślny + Cyfrowe rachunki kosztów zostały dezaktywowane i usunięte. + „Forum e-recept” + Nie ma możliwości przygotowania zastępczego + Możliwość wyboru preparatu zastępczego + Przygotowanie zastępcze (Aut idem) + Farmaceuci mają obowiązek przyznania pierwszeństwa wydawaniu leków, na które zakład ubezpieczeń zdrowotnych pacjenta zawarł umowę rabatową z producentami leków. Nie dotyczy to sytuacji, gdy lekarz nie umieścił na recepcie słowa „Aut idem”, co nie ma miejsca w przypadku Twojej recepty. + Twój lekarz zadecydował, że powinieneś otrzymać przepisany lek. Apteka nie powinna przeprowadzać wymiany na podstawie umowy rabatowej („Aut idem”). + Zezwalaj na zrzuty ekranu + Jeśli zezwolisz na zrzuty ekranu, ostatnia otwarta strona pozostanie widoczna w tle podczas przełączania aplikacji. W razie potrzeby Twoje dane osobowe mogą być widoczne. Dlatego zalecamy, aby nie zezwalać na zrzuty ekranu. + Zezwól + Anuluj + Zmień klawiaturę na naklejki lub użyj emotikonów na swoim zdjęciu profilowym. + Wybierz zdjęcie profilowe + Jak chcesz kontynuować? + Wybierz zdjęcie + kamera + Emoji + Anuluj + Otwórz ustawienia + Używać + Zrób zdjęcie + Edytuj zdjęcie profilowe + Założono, że %s minut temu + Zaakceptowano dnia %s + Właśnie zaakceptowałem + Zaakceptowano o godzinie %s + Identyfikator zdrowia + Wybierz ubezpieczenie + Jeśli rejestracja przy użyciu identyfikatora zdrowotnego nie przebiegnie zgodnie z oczekiwaniami, postępuj zgodnie ze wskazówkami z naszego centrum pomocy. + Pomoc + Pomoc + Wskazówki dotyczące rejestracji w aplikacji ubezpieczeniowej + Za identyfikator medyczny odpowiedzialna jest Twoja firma ubezpieczeniowa. Skontaktuj się z nimi, jeśli masz jakiekolwiek pytania dotyczące rejestracji. Oto kilka wypróbowanych i przetestowanych wskazówek: + Należy pamiętać, że w zależności od ubezpieczenia może być wymagana osobna aplikacja. Aby dowiedzieć się, co to jest, skontaktuj się ze swoją firmą ubezpieczeniową. + Uruchom aplikację kasową i zaloguj się w niej raz, zanim rozpoczniesz rejestrację w aplikacji e-recepta. + Przełączanie pomiędzy rejestracją przy użyciu identyfikatora zdrowotnego i karty zdrowia może powodować problemy. Dlatego przed zmianą opcji logowania prosimy najpierw aktywnie wylogować się ze swojego profilu. + Jeśli Twojego ubezpieczenia nie ma na liście, możesz alternatywnie zarejestrować się za pomocą swojej karty zdrowia i odpowiedniego PIN-u. + Jeżeli aplikacja kasowa nie przekieruje Cię z powrotem do aplikacji e-recepty, koniecznie zgłoś ten błąd swojemu ubezpieczycielowi. + Jeśli nie można uzyskać dostępu do aplikacji ubezpieczeniowej, pomocne może być przejście do ustawień przeglądarki w smartfonie i zezwolenie na otwarcie linków. + Jeśli Twój smartfon działa pod kontrolą systemu Android 14, pomocne może być zezwolenie na otwieranie linków w ustawieniach. + Otwórz ustawienia + Niestety nie udało się + Spróbuj ponownie później. + Komunikaty o błędach podczas ładowania. + Wybierz emoji lub tekst + Zapisz + Proszę się zalogować, aby kontynuować + Dalej + Dzwonić + Pisać\nPoczta + Do apteki + Apteka + Dzisiaj + Jutro + Tutaj\ntrasa + Usuń receptę i paragon kosztowy + Jeśli usuniesz recepturę, powiązany z nią rachunek kosztów również zostanie usunięty. + Wiadomości ze wszystkich profili są teraz wyświetlane razem + Nowe funkcje + Nowe funkcje + Stale pracujemy nad nowymi funkcjami. Podziel się z nami swoją opinią i pomóż nam stworzyć lepszą aplikację. + Zamówienia dla każdego + W sekcji Zamówienia możesz teraz zobaczyć zamówienia dla wszystkich profili razem i przeglądać je znacznie łatwiej + + Brak zwrotu kosztów + Co do zasady ubezpieczenie zdrowotne nie pokrywa kosztów realizacji tej recepty. Jako pacjent jesteś zatem odpowiedzialny za pełną opłatę. To, czy koszty zostaną zwrócone w ramach dodatkowego ubezpieczenia, czy świadczeń ustawowych, należy sprawdzić indywidualnie. + Twoje ubezpieczenie nie pokryje żadnych kosztów związanych z tą receptą. + + "Twoje ubezpieczenie nie pokryje recepty %s " + + + „Twoje ubezpieczenie nie pokryje recept %s .” + + Spowodowany + Data + Wypadek + Wypadek przy pracy + Choroba przemysłowa + Aktualności + Brak wiadomości + Nie masz jeszcze żadnych wiadomości. + 🎉 Twoje zamówienie jest gotowe do odbioru. Pokaż ten kod odbioru, aby się zidentyfikować. + Niestety wiadomość Twojej apteki była pusta. Skontaktuj się z apteką. + Apteka udostępniła Ci link. + Aktualności + Zamówienie + Wiadomości + Brak połączenia z Internetem + Aby wyświetlić podłączone urządzenia, musisz mieć połączenie z systemem biometrycznym. + Recepta została usunięta dnia %s + Usunięto + Brak recept + Nie masz przepisów w archiwum. + Twoje rachunki za wydatki zostały usunięte + Rachunek kosztów + Wymagana rejestracja + Zaloguj się, aby zobaczyć podłączone urządzenia. + Rejestr + Rejestr + Połącz się z %s Ubezpieczeniem + Karta zdrowia na smartfonie + Nie zalogowano + Zarejestrowany + odświeżać + Otrzymasz protokoły dostępu, jeśli jesteś zalogowany w aplikacji E-recepta. + Wymagana rejestracja + Zaloguj się przy użyciu swojej karty zdrowia i zapisz dane logowania za pomocą danych biometrycznych, aby wyświetlić podłączone urządzenia. + %1$s x Rano + %1$s x Obiad + %1$s x Wieczór + %1$s x W nocy + O ile lekarz nie zalecił inaczej, instrukcję użycia należy rozumieć w następujący sposób: + Twój lekarz przekazał Ci te informacje na temat przyjmowania leku. + Na recepcie nie ma informacji o tym, jak przyjmować lek. + Twój lekarz zauważył, że otrzymałeś instrukcje dotyczące przyjmowania tego leku, który nie jest wystawiony na receptę. Może to być na przykład uwzględnione w planie leczenia. + Brak danych + DJ + Instrukcje użytkowania + " %s - %s" + Przypomnienie o przychodach + Weź przypomnienia + Leki doustne + O przypomnieniach o przyjęciu + Poranek + Południe + Popołudnie + Wieczorem + Lek + Informacje o dawkowaniu + 1 + %s / %s + 1 dawka + Zmień dawkowanie + Anuluj + Przypomnienie o lekach + Harmonogram leków + Należy przyjmować dawkę zgodnie z zaleceniem lekarza + O godzinie %s weź %s x + Wybierz daty + Dalej + Anulować + A + Z + Śledzenie + Nieograniczony + Indywidualnie + Pierwszy dzień + Ostatni dzień + Dodaj czas + Brak przypomnień o spożyciu + Możesz ustawić przypomnienia o swoich receptach + włączony + wyłączony + Pamiętaj mnie + Wyłącz optymalizację baterii dla tej aplikacji. + Jeśli nie aktywujesz tej opcji, przypomnienia o lekach mogą wydawać się niewiarygodne. + Anuluj + Zezwól + Instrukcje użytkowania + Brak danych + informacja + Pamiętaj także o trybie optymalizacji baterii + Zmień dawkowanie + Ilość + formularz + zakończył się + nieograniczony + do %s + Powtórz %s + Tam, gdzie to możliwe, do obliczeń wykorzystujemy informacje zapisane na recepcie. + Anuluj + Zapisz + Do końca paczki + Czas + dawka + Zobacz leki + Czy wziąłeś już leki? + Weź przypomnienia + Brak aktywnych pamięci + Nie masz dzisiaj przypomnień do zrobienia + Wybierz datę rozpoczęcia + Wybierz datę zakończenia + Powitanie + w aplikacji do e-recept + Spróbuj ponownie + Wprowadź hasło + Upewnij się, że każda osoba, której możesz dzielić to urządzenie i która zna Twoje dane logowania, ma również dostęp do Twoich przepisów. + Wprowadź hasło + Wprowadź hasło, aby odblokować aplikację. + hasło + Hasło + Wprowadź hasło, aby zabezpieczyć aplikację. + Pokaż hasło + Powtórz hasło + Zapomniałeś hasła? Usuń aplikację, a następnie zainstaluj ją ponownie. Możesz dowiedzieć się dlaczego w naszym %s. + Wprowadź hasło + Hasło musi mieć co najmniej osiem znaków + Siła hasła jest niewystarczająca + Siła hasła wystarczająca + Tworzenie kopii zapasowych urządzenia nie jest możliwe + Najpierw skonfiguruj kopię zapasową urządzenia. + Anulować + Ustawienia + Siła hasła bardzo dobra + Bezpieczeństwo aplikacji + Kopia zapasowa urządzenia + Wybierz co najmniej jedną metodę tworzenia kopii zapasowej aplikacji. + hasło + Zmień hasło + Aby usunąć, należy nawiązać połączenie z serwerem Prescription Server + Towar jest gotowy + Towary są gotowe od %s + Towar był już gotowy + Towary są gotowe od %s minut + Towary są gotowe od %s + Wyłączone, ponieważ nie wprowadzono hasła. + Wyłączone, ponieważ hasło jest za słabe. + Wyłączone, ponieważ hasła się nie zgadzają. + Badać + Rejestr dawstwa narządów + Otwarty rejestr dawstwa narządów? + Zostaniesz przekierowany do rejestru dawców narządów. Aby przeglądać i zmieniać dane dotyczące dawstwa narządów, musisz się tam zalogować. + Otwarte + Anulować + Funkcja nieaktywna w trybie demonstracyjnym diff --git a/app/features/src/main/res/values-ro/strings.xml b/app/features/src/main/res/values-ro/strings.xml index 4f62e68d..7866a99f 100644 --- a/app/features/src/main/res/values-ro/strings.xml +++ b/app/features/src/main/res/values-ro/strings.xml @@ -27,7 +27,7 @@ Nu anulați Să mergem De ce ai nevoie: - Introduceți numărul de acces al cardului + Introduceți numărul de acces introduceți codul PIN Încearcă din nou Eroare de conectare la server. @@ -57,26 +57,23 @@ imprima editor gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin - Director general: Dr. Florian Hartge\n Judecătoria de înregistrare: Judecătoria Berlin-Charlottenburg\n Număr registru comercial: HRB 96351\n Număr de identificare TVA: DE241843684 + Management: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nJudecătoria de înregistrare: Judecătoria Berlin-Charlottenburg\nNumăr registru comercial: HRB 96351\nNumăr de identificare TVA: DE241843684 Responsabil pentru conținut - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge a lua legatura Înștiințare Ne străduim să folosim un limbaj echitabil de gen. Dacă observați vreo eroare, ne-am bucura să vă primim un e-mail. Platforma modernă a Germaniei pentru medicina digitală Scrie email Deschide site-ul web - Bine ati venit - Începeți înregistrarea Deblocați Inregistreaza-te Anulare - Securitate Legal imprima protejarea datelor Termeni de utilizare - Detalii + Detalii reteta Marcați ca răscumpărat Marcați ca nerăscumpărat Forma de dozare @@ -98,8 +95,6 @@ Numărul plantei Număr de telefon Adresa de e-mail - Accident de muncă - Ziua accidentului Firma accidentului sau numărul angajatorului Doriți să ștergeți definitiv această rețetă? Șterge @@ -122,7 +117,6 @@ Deschideți scanerul pentru rețete Setări Suprimați capturile de ecran - Împiedică afișarea unei imagini de previzualizare la schimbarea aplicațiilor Permiteți E-Prescription să vă analizeze comportamentul de utilizare în mod anonim? Informații tehnice Securitatea datelor dumneavoastră de prescripție @@ -144,23 +138,15 @@ Analiza anonimă rămâne dezactivată %s Vă mulțumim pentru sprijin! Inregistreaza-te - Vă rugăm să vă identificați pentru a descărca rețete. - Notă pentru farmacii: Obținem datele de contact și informațiile despre farmacii de la mein-apothekenportal.de al Asociației Germane de Farmacie Ați descoperit o eroare sau doriți să corectați datele? + Vă rugăm să vă identificați. + Notă pentru farmacii: Obținem datele de contact și informații despre farmacii de la mein-apothekenportal.de. Ați descoperit o eroare sau doriți să corectați datele? Află mai multe Farmacii Din păcate, nu a funcționat \uD83D\uDE15 Vă rugăm să încercați din nou. - Introdu parola - Mai departe Accesibilitate - Zoom - Vă permite să măriți aplicația prin pinch-to-zoom. - parola - Asigurați-vă datele cu o parolă la alegere. - parola + Activați mărirea Salvați - Arata parola - Repetați parola Recomandări: %s Scrie e-mail Când trimiteți mesajul dvs., sunt transmise următoarele informații despre hardware-ul și sistemul de operare utilizat: @@ -171,7 +157,7 @@ Expediere filtru Filtru - Nicio locație disponibilă + Partajați locația Înțeles Parola repetată se potrivește Eroare 20 10 76631 @@ -182,8 +168,6 @@ Au fost detectate %s încercări de conectare nereușite. - Alegeți cel mai bun backup pentru dispozitiv - Aceasta poate fi o amprentă, un model de glisare sau ceva similar Jetoane Jetoane de acces Jetoane SSO @@ -191,7 +175,7 @@ nu este disponibil niciun simbol SSO copiat în clipboard Faceți clic pentru a copia jetonul în clipboard - Valabil doar azi + Rambursabil doar astăzi Permite nicio conexiune la server Vă rugăm să încercați din nou în câteva minute @@ -258,7 +242,7 @@ %s rețete noi Rambursabil - În răscumpărare + Este în curs de procesare Răscumpărat Necunoscut Vizualizați jurnalele de acces @@ -270,7 +254,7 @@ Rețeta este în curs de desfășurare și nu poate fi ștearsă Accept Se pare că nu a funcționat - Suntem conștienți că legătura cu cardul de sănătate are capcanele ei. În viitor, înregistrarea ar trebui să fie posibilă și prin intermediul unei aplicații de asigurări de sănătate deja autentificate. \n\n De asemenea, lucrăm pentru a ne asigura că rețetele pot fi răscumpărate digital fără înregistrare. \n\n Ați observat ceva în timpul acestui proces pe care ați dori să ne împărtășiți? Vă rugăm să ne scrieți, am fi bucuroși să primim feedback foarte critic. + Suntem conștienți că legătura cu cardul de sănătate are capcanele ei. În viitor, înregistrarea ar trebui să fie posibilă și prin intermediul unei aplicații de asigurări de sănătate deja autentificate.\n\nDe asemenea, lucrăm pentru a ne asigura că rețetele pot fi răscumpărate digital fără înregistrare.\n\nAți observat ceva în timpul acestui proces pe care ați dori să ne împărtășiți? Vă rugăm să ne scrieți, am fi bucuroși să primim feedback foarte critic. Sfaturi de conectare Îmbunătățiți puterea conexiunii Dacă este necesar, îndepărtați capacul de protecție. @@ -293,7 +277,7 @@ Scanat pe %s Marcat ca valorificat pe %s Cum vrei să continui? - Ordin + Trimite la farmacie Disponibil în curând Rezervați acum pentru colectare sau primiți-l prin curier sau transport Salvează pentru comanda ulterioară @@ -324,13 +308,11 @@ https://www.openstreetmap.org/copyright Protecția și utilizarea datelor Mai departe - Ați primit PIN-ul cardului de sănătate de la compania dumneavoastră de asigurări de sănătate utilizând o procedură securizată, cum ar fi Postident. + A trebuit să comandați în mod activ PIN-ul cardului de sănătate de la compania de asigurări de sănătate și apoi să îl primiți printr-un proces securizat, cum ar fi Postident. Nu s-a primit niciun cod PIN cod PIN Verificați conexiunea la internet a dispozitivului și setările de oră/dată. - Pentru a vă autentifica, apăsați pe „Deblocare”. Închis pe dinafară? Vă rugăm să vă verificați acreditările biometrice pe acest dispozitiv. - Aţi uitat parola? Vă rugăm să ștergeți aplicația și apoi să o reinstalați. Puteți afla de ce în %s nostru. zona de ajutor Dimensiunea pachetului și unitate ingredient activ @@ -343,10 +325,6 @@ Anulat Înștiințare Ajutați-ne să îmbunătățim această aplicație - Introdu parola - Parola trebuie să aibă cel puțin opt caractere - Puterea parolei nu este suficientă - Puterea parolei este suficientă Parola este vizibilă Parola nu este vizibilă biometrie @@ -405,7 +383,7 @@ Cu PIN-ul dvs. ați primit un PUK de 8 cifre de la compania dvs. de asigurări. Alegeți codul PIN nou Puteți alege singur noul PIN (6 până la 8 cifre). - Ți-ai amintit codul PIN? + V-ați amintit codul PIN? Vă rugăm să scrieți codul PIN și să-l păstrați într-un loc sigur. Anulare Bine @@ -452,9 +430,6 @@ Trimis chiar acum Trimis la %s oră Nu mai este valabil - Înregistrați-vă cu aplicația - Alegeți asigurarea - Nu ați găsit ceea ce căutați? Această listă este în continuă extindere. Înregistrarea cu cardul de sănătate este deja acceptată de fiecare companie de asigurări de sănătate. Feedback din aplicația e-rețetă Așteptăm cu nerăbdare feedback-ul dvs. Vă rugăm să folosiți următorul spațiu și să fiți cât mai precis posibil: PUK @@ -474,7 +449,7 @@ Înștiințare Accept Securitatea datelor dumneavoastră de prescripție - „Această aplicație folosește cel mai sigur senzor biometric oferit de dispozitiv pentru a vă securiza acreditările într-o zonă protejată a dispozitivului de stocare.” + \Această aplicație folosește cel mai sigur senzor biometric furnizat de dispozitivul dvs. pentru a vă securiza acreditările într-o zonă protejată a stocării dispozitivului.\ Securitatea biometrică a datelor dvs. de acces vă permite să deschideți această aplicație în viitor, să vizualizați, să preluați, să răscumpărați sau să ștergeți rețetele fără un card de sănătate și să introduceți codul PIN. Vă rugăm să vă asigurați că persoanele cu care puteți partaja acest dispozitiv și ale căror caracteristici biometrice pot fi stocate pe acest dispozitiv au și acces la rețetele dvs. care din pacate nu a functionat @@ -487,7 +462,7 @@ Nume de familie Asigurare Numar de asigurare - Număr de acces card + Număr de acces Inregistreaza-te Deconectați-vă Salvați @@ -503,8 +478,7 @@ conexiune pierdută Conectați-vă la serverul de rețete acum? Fără jetoane - Veți primi un simbol atunci când sunteți conectat la serviciul de prescripție medicală.\n - Comenzi + Veți primi un simbol atunci când sunteți conectat la serviciul de prescripție medicală. Selectați codul PIN dorit Deblocați cardul Selectați PIN @@ -521,26 +495,26 @@ Cod de preluare primit Mesajul nu poate fi afișat Vă rugăm să contactați farmacia ( %s ). - Afișați linkul coșului de cumpărături + Link la farmacie Afișați codul de ridicare Arată mesajul %s la ora %s - Rețeta trimisă către %s . Procesul de răscumpărare digitală este încă necunoscut pentru multe farmacii. Dacă nu primiți răspuns până mâine, vă recomandăm să sunați pentru a vă informa ca măsură de precauție. + Rețeta trimisă către %s . Unele farmacii nu au încă o opțiune de răspuns digital. Dacă nu există niciun răspuns până mâine, vă rugăm să sunați ca măsură de precauție. Prezentare generală a comenzii Nou Curs Ordinea - Gratuit pentru apelant. Orele de serviciu: Luni - Vineri 8:00 - 20:00, cu excepția sărbătorilor naționale + Gratuit pentru apelant. Orele de serviciu: Luni - Vineri 8:00 a.m. - 8:00 p.m., cu excepția sărbătorilor federale Farmacie Selectați codul PIN dorit PIN-ul dorit a fost salvat Momentan deschis și lângă mine Filtreaza dupa … - incepe cautarea + Căuta Atribuire directă Farmacii Număr de telefon (opțional) - Căutați după nume sau adresă + Căuta Nu există informații valide despre farmacie Nu au fost găsite informații actuale despre această farmacie. Înregistrarea pentru această farmacie va fi ștearsă. Bine @@ -560,16 +534,15 @@ Îmbunătățiri ale produsului Analiza anonima Ajutați-ne să îmbunătățim această aplicație. Toate datele de utilizare sunt colectate anonim și sunt folosite exclusiv pentru a îmbunătăți experiența utilizatorului. - Securitatea aplicației setari personale Accesibilitate Îmbunătățiri ale produsului Rețetă adăugată Reteta deja disponibila A apărut o eroare la import + Număr de acces Șterge Rețetă scanată - Posibilă pregătire pentru înlocuire PIN uitat %s Rețetă @@ -586,7 +559,7 @@ Puteți modifica oricând această decizie din setările sistemului. Continua Accept - Această aplicație folosește cea mai sigură metodă oferită de dispozitivul dvs. + Aplicația folosește cea mai sigură metodă pe care ați configurat-o pe dispozitivul dvs. Salvați Alege medicament @@ -614,15 +587,13 @@ Ce este o misiune directă? Cu trimitere directă, o rețetă de la un cabinet sau spital este completată direct la o farmacie. Asigurații nu trebuie să întreprindă nicio măsură și nu pot interveni în procesul de răscumpărare. \n\n Recomandările directe sunt enumerate în aplicația e-rețetă pentru a face tratamentul mai transparent pentru dvs. Taxa de serviciu de urgenta - Uneori este nevoie de grabă. Unele rețete pot fi completate fără plata suplimentară a unei taxe de serviciu de urgență, de exemplu noaptea sau de sărbători. + Dacă o rețetă se eliberează între orele 20:00 și 6:00 sau în zilele de duminică și de sărbătorile legale, se poate percepe o taxă suplimentară de 2,50 euro. Medicamente supuse coplății Scutit de plata suplimentara - Cei cu asigurare legală de sănătate trebuie să plătească o plată suplimentară de până la zece euro pentru medicamentele eliberate pe bază de rețetă. \n\n Valoarea plății suplimentare depinde de prețul medicamentului dumneavoastră. Trebuie să plătiți singur pentru medicamentele care costă mai puțin de 5 EUR.\n Pentru medicamentele care sunt mai scumpe, trebuie să plătiți zece la sută din preț, dar cel puțin 5 euro și maxim 10 euro. \n\n Copiii și tinerii cu vârsta sub 18 ani sunt, în general, scutiți de plata suplimentară. \n\n Dacă costurile anuale pentru medicamente depășesc limita povara financiară, puteți fi scutit de coplăți. Discutați cu compania dumneavoastră de asigurări de sănătate despre acest lucru. + Persoanele cu asigurare legală de sănătate plătesc de obicei maximum 10 euro pentru medicamentele prescrise. Se pot aplica taxe mai mari dacă este solicitat un medicament de la un anumit producător care nu este acoperit de un acord de reducere a asigurării de sănătate („solicitați medicament”). \n\n Copiii și tinerii sub 18 ani sunt scutiți de plăți suplimentare. \n\n Dacă rețetele sunt răscumpărate mai târziu de 28 de zile de la eliberare, costurile trebuie suportate integral. \n\n Dacă cheltuiți o mulțime de medicamente pe parcursul anului, puteți solicita o scutire de la coplată de la compania dumneavoastră de asigurări de sănătate Sunteți scutit de la plata unei coplăți pentru acest medicament. Compania dumneavoastră de asigurări de sănătate va acoperi costul medicamentului. Cât timp este valabilă această rețetă? În această perioadă, vă puteți răscumpăra rețeta în orice farmacie cu o plată suplimentară maximă de 10 EUR. - Posibilă pregătire pentru înlocuire - Datorită cerințelor legale din partea companiei dumneavoastră de asigurări de sănătate, vi se poate oferi o alternativă cu același ingredient activ. \n\n Medicamentele pot arăta și pot fi numite diferit, au prețuri și producători diferiți, dar conțin totuși același ingredient activ. Ingredientul activ în sine și doza sunt cruciale pentru efectul medicamentelor în organism. Pacienții primesc adesea un alt medicament la farmacie decât cel prescris de medic - cu condiția ca medicația să fie comparabilă. Pot exista motive terapeutice și economice pentru schimbare. Rețetă scanată Rețetele importate dintr-o copie hârtie nu pot afișa informații personale sau medicale din motive de securitate. \n\n Conectați-vă la această aplicație cu cardul de sănătate sau aplicația de asigurare pentru a vedea toate informațiile conținute în rețetă. Reteta incorecta @@ -632,7 +603,7 @@ telefon site-ul web Poștă - Sortarea după distanță nu este posibilă. + Partajați locația dvs. pentru a găsi farmacii din apropiere. Bine Introduceți codul PIN actual PIN introdus greșit @@ -650,12 +621,10 @@ Card de sanatate biometrie Neconectat - Ne interesează opinia dumneavoastră. Vă rugăm să acordați cinci minute pentru a completa sondajul nostru. Vă mulțumesc foarte mult anticipat. Notă de avertizare Farmacie adăugată la favorite Farmacie eliminată din favorite Farmaciile mele - Puterea parolei este foarte bună Operația de scriere nu a reușit PIN-ul nu a putut fi salvat Raport @@ -674,7 +643,6 @@ Valorificat pe %s Răscumpărat chiar acum Valorificat la ora %s - Comenzi Această rețetă a fost eliberată ca parte a unui tratament pentru dumneavoastră. Taxa de serviciu de urgenta Această rețetă nu poate fi eliberată la o farmacie noaptea fără plata suplimentară a unei taxe de serviciu de urgență. @@ -688,7 +656,7 @@ Primiți rețete digital? Trageți în jos ecranul pentru a reîmprospăta. Fara retete - Conectați-vă pentru a primi rețete automat sau adăugați o nouă rețetă folosind ⊕ din colțul de sus. + Înscrieți-vă pentru a primi rețete automat. Inregistreaza-te Arhiva rețetelor Poate mai târziu @@ -725,9 +693,9 @@ Faceți clic pe afișaj pentru a sări peste sfatul instrument care apare. Cum să răscumpărați? Cum ați dori să primiți medicamentele? - Răscumpărați direct - Rambursați medicamentele la fața locului - Ordin + Afișați codul + Scaneaza-l la farmacie + Trimite la farmacie Rezervați sau primiți-l Gata Cod de colectare @@ -794,7 +762,7 @@ Aceasta va șterge toate chitanțele de cheltuieli de pe acest dispozitiv și de pe server. Primiți chitanțe de cost Încasările dvs. de cost sunt salvate și pe serverul de rețete. - Primit + Activati Total: %s %s Alege Despică @@ -843,17 +811,17 @@ PIN asociat este necesar Poate fi răscumpărat doar mâine ca plătitor propriu Au mai rămas doar %s zile pentru a valorifica ca plătitor propriu - \nÎncă poate fi răscumpărat ca plătitor propriu pentru %s zile\n - Valabil numai pentru %s zile - \nValabil pentru %s zile rămase\n - Valabil doar maine + Încă poate fi răscumpărat ca plătitor propriu pentru %s zile + Au mai rămas doar %s zile pentru valorificare + Încă poate fi valorificat timp de %s zile + Rambursabil doar mâine Se aplică taxe Ia asigurare Rețetele au fost transferate cu succes. Rețeta nu poate fi procesată. Vă rugăm să încercați din nou. Poate fi necesar să alegeți o altă farmacie. Rețeta nu poate fi procesată. Farmacia raportează o eroare necunoscută. Dacă este necesar, încercați o altă farmacie. Rețeta a fost respinsă de farmacie. Rețeta poate fi invalidă sau adresa dumneavoastră de livrare sau informațiile de contact pot fi invalide. - Nu se poate valorifica, vă rugăm să vă verificați conexiunea la internet. + Încercați din nou și eventual alegeți o altă farmacie. Dacă eroarea persistă, vă rugăm să contactați asistența. Rețeta a fost transferată cu succes. Cu toate acestea, farmacia raportează o eroare de procesare. Vă rugăm să contactați farmacia. Rețeta a fost respinsă de farmacie. Rețeta a fost deja răscumpărată. Rețeta a fost respinsă de farmacie. Rețeta a fost ștearsă. @@ -886,7 +854,6 @@ Activați modul demo Nu acum Căutare - Este necesar un smartphone cu NFC Primiți chitanțe de cost în format digital Odată ce chitanțele de cost au fost activate, le veți găsi aici după ce vă răscumpărați rețeta. Activati @@ -894,11 +861,11 @@ Imediat ce farmacia a depus chitanta de cost, aceasta va aparea aici. Chitanța costurilor Primiți chitanțe de cost în format digital - Notă: nu veți mai primi chitanțele de cost ca tipărire în\n Farmacie.\n + Notă: nu veți mai primi chitanțele dvs. de cost ca imprimare la farmacie. Activati Poate mai târziu acord - Cu acordul dumneavoastră, vă veți abține de la a-l imprima în farmacie\n și trimiteți chitanțele dvs. de cost în format digital în viitor.\n + Cu acordul dvs., vă veți abține de la a le imprima la farmacie și veți trimite chitanțele de cost în format digital în viitor. Anulare De acord Ieși din modul demo @@ -913,17 +880,17 @@ Fara Internet Nu există conexiune la Internet. Cerere incorectă - A apărut o problemă cu cererea. Lucrăm la o soluție.\n + A apărut o problemă cu cererea. Lucrăm la o soluție. serverul nu răspunde Vă rugăm să încercați din nou în câteva minute. Eroare de conexiune - În prezent, nicio informație nu poate fi preluată. Vă rog\n încercați mai târziu.\n + În prezent, nicio informație nu poate fi preluată. Vă rugăm să încercați din nou mai târziu. Respins - Serverul a respins cererea dvs. Vă rugăm să încercați\n din nou mai tarziu.\n + Serverul a respins cererea dvs. Vă rugăm să încercați din nou mai târziu. Actualizați aplicația - Pentru a utiliza această funcție, actualizați aplicația.\n + Pentru a utiliza această funcție, actualizați aplicația. Autentificare eșuată - Serverul a avut o problemă la conectare. te rog ia legatura\n din nou.\n + Serverul a avut o problemă la conectare. Vă rugăm să vă conectați din nou. Inregistreaza-te Copiați adresa URL Conectați-vă la aplicația externă de asigurări de sănătate. @@ -949,12 +916,286 @@ Recomandat ID digital de sănătate Ajustați setările? - pagina 404? Vă rugăm să activați deschiderea linkurilor în setările dvs. pentru a continua. Deschide setările Versiune de aplicație învechită Versiunea dvs. a aplicației este învechită și nu mai este acceptată. Pentru a continua să utilizați rețeta electronică, vă rugăm să actualizați aplicația. A updata - CAN-ul dvs. are %s cifre. + Numărul dvs. de acces are %s cifre. PIN-ul dvs. poate avea %s până la %s cifre. + Ridica + Curier + Expediere + Este necesar un smartphone cu NFC + Nu am putut procesa solicitarea de ștergere a rețetei. Lucrăm la o soluție ( %s ). + Ceva a mers prost la ștergerea rețetei mele, am primit codul de eroare %s . Vă rugăm să mă anunțați când ștergerea este posibilă din nou. + Mai departe + Cerere incorectă + Raport + Ștergeți local + Fara Internet + Rețeta nu a putut fi ștearsă. Vă rugăm să vă verificați conexiunea la internet și să încercați din nou. + Încearcă din nou + Bine + Re logare + Trebuie să fii autentificat pentru a șterge rețeta (401). + Inregistreaza-te + Anulare + Ștergerea imposibilă + Nu aveți voie să ștergeți rețeta în acest moment deoarece se află la farmacie pentru a fi completată sau este o atribuire directă în așteptare (403). + Prea multe cereri + Ați încercat să ștergeți rețeta de prea multe ori. Vă rugăm să încercați din nou mai târziu (429). + Ștergeți rețeta + Hopa... + „A apărut o eroare neașteptată de server. Vă rugăm să încercați din nou mai târziu (500).” + Securitatea aplicației nu este posibilă. În prealabil, configurați securitatea biometrică (de exemplu, amprenta digitală) pe dispozitiv. + Backup-ul biometric nu este posibil pentru acest dispozitiv, deoarece nu este acceptat de dispozitivul dvs. + Pericol! Telefonul dvs. nu este bine protejat. + Detaliile dvs. de conectare sunt salvate pe telefon. Pentru a proteja accesul, este utilizată metoda de conectare preferată. Această metodă de conectare, de exemplu modelul de glisare, nu este foarte sigură. Utilizați funcția „Salvați detaliile de conectare” pe propriul risc. + Dacă încă folosiți funcția „Salvați datele de conectare”, veți putea să vizualizați, să accesați, să răscumpărați sau să ștergeți rețetele cu aplicația e-prescription în viitor fără card de sănătate și introducerea codului PIN. + Locația nu a fost găsită + Trimite la farmacie + Afișați codul + Limba + Limba + Limba germana + Arabic + Bulgară + Ceh + Danez + Engleză + Limba franceza + Ebraică + Italiană + Olandeză + Lustrui + Română + Rusă + Turc + Ucrainean + Mod implicit + Chitanțe de costuri digitale au fost dezactivate și șterse. + „Forum de prescriere electronică” + Nu este posibilă nicio pregătire înlocuitoare + Posibilă pregătire pentru înlocuire + Preparat înlocuitor (Aut idem) + Farmaciştii sunt obligaţi să acorde prioritate eliberării medicamentelor pentru care casa de asigurări de sănătate a pacientului a încheiat un acord de reduceri cu producătorii de medicamente. Acest lucru nu se aplică numai dacă medicul exclude „Aut idem” pe rețetă, ceea ce nu este cazul cu rețeta dumneavoastră. + Medicul dumneavoastră a stabilit că trebuie să primiți medicamentele prescrise. Farmacia nu ar trebui să efectueze schimburi pe baza unui acord de reducere („Aut idem”). + Permite capturi de ecran + Dacă permiteți capturi de ecran, ultima pagină pe care ați deschis-o va rămâne vizibilă în fundal când comutați între aplicații. Dacă este necesar, datele dumneavoastră personale pot fi vizibile. Prin urmare, vă recomandăm să nu permiteți capturile de ecran. + Permite + Anulare + Comutați tastatura la autocolante sau folosiți emoji-uri pentru fotografia de profil. + Alege poza de profil + Cum vrei să continui? + Selectați fotografia + aparat foto + Emoji + Anulare + Deschide setările + Utilizare + Fa o fotografie + Editează poza de profil + Se presupune că acum %s minute + Acceptat pe %s + Tocmai acceptat + Acceptat la ora %s + HealthID + Alegeți asigurarea + Dacă înregistrarea cu ID de sănătate nu decurge conform așteptărilor, vă rugăm să urmați sfaturile din centrul nostru de ajutor. + Ajutor + Ajutor + Sfaturi pentru înregistrarea cu aplicația de asigurări + Compania dvs. de asigurări este responsabilă pentru ID-ul de sănătate. Vă rugăm să-i contactați dacă aveți întrebări despre înregistrare. Iată câteva sfaturi încercate și testate: + Vă rugăm să rețineți că, în funcție de asigurarea dvs., poate fi necesară o aplicație separată. Vă rugăm să verificați cu compania dvs. de asigurări pentru a afla despre ce este vorba. + Porniți aplicația de casă de marcat și conectați-vă acolo o dată înainte de a începe înregistrarea în aplicația e-prescription. + Comutarea între înregistrarea cu ID de sănătate și cardul de sănătate poate duce la probleme. Prin urmare, vă rugăm mai întâi să vă deconectați activ de la profilul dumneavoastră înainte de a schimba opțiunea de conectare. + Dacă asigurarea dumneavoastră nu este pe listă, vă puteți înregistra alternativ cu cardul de sănătate și PIN-ul asociat. + Dacă aplicația de casă de marcat nu vă redirecționează înapoi la aplicația de prescriere electronică, asigurați-vă că raportați această eroare companiei dumneavoastră de asigurări. + Dacă aplicația de asigurare nu poate fi accesată, poate fi util să accesați setările browserului smartphone-ului dvs. și să permiteți deschiderea linkurilor. + Dacă smartphone-ul dvs. rulează Android 14, ar putea fi util să permiteți deschiderea linkurilor în setări. + Deschide setările + care din pacate nu a functionat + Vă rugăm să încercați din nou mai târziu. + Mesaje de eroare la încărcare. + Vă rugăm să alegeți un emoji sau un text + Salvați + Vă rugăm să vă conectați pentru a continua + Mai departe + Apel + Scrie\nPoștă + La farmacie + Farmacie + Astăzi + Mâine + Aici\ntraseu + Ștergeți rețeta și chitanța de cost + Dacă ștergeți rețeta, se șterge și chitanța de cost asociată. + Mesajele din toate profilurile sunt acum afișate împreună + Functii noi + Functii noi + Lucrăm constant la noi funcții. Împărtășește-ți părerea cu noi și ajută-ne să construim o aplicație mai bună. + Comenzi pentru toată lumea + Sub Comenzi puteți vedea acum comenzile pentru toate profilurile împreună și le puteți vizualiza mult mai ușor + + Fără rambursare a costurilor + De regulă, asigurarea de sănătate nu acoperă costurile pentru această rețetă. În calitate de pacient, sunteți, prin urmare, responsabil pentru plata integrală. Dacă costurile vor fi rambursate ca parte a asigurării suplimentare sau a beneficiilor statutare trebuie verificat individual. + Asigurarea dumneavoastră nu va acoperi niciun cost pentru această rețetă. + + „Asigurarea dumneavoastră nu va acoperi prescripția %s ” + + „Asigurarea dumneavoastră nu va acoperi rețetele %s ”. + + Cauzat + Data + Accident + Accident de muncă + Boala industrială + Ştiri + Nicio veste + Încă nu aveți niciun mesaj. + 🎉 Comanda ta este gata de ridicare. Vă rugăm să afișați acest cod de ridicare pentru a vă identifica. + Din păcate, mesajul de la farmacie a fost gol. Vă rugăm să contactați farmacia dumneavoastră. + Farmacia v-a oferit un link. + Ştiri + Comanda + Mesaje + Fără conexiune la internet + Pentru a vizualiza dispozitivele conectate, trebuie să fiți conectat la datele biometrice. + Rețeta a fost ștearsă pe %s + Șters + Fara retete + Nu aveți rețete în arhivă. + chitanțele dvs. de cheltuieli au fost șterse + Chitanța costurilor + Este necesară înregistrarea + Vă rugăm să vă conectați pentru a vedea dispozitivele conectate. + Inregistreaza-te + Inregistreaza-te + Conectați-vă la %s Asigurări + Card de sănătate pe smartphone + Neconectat + Înregistrat + reîmprospăta + Veți primi jurnalele de acces dacă sunteți autentificat la serviciul de prescripție medicală. + Este necesară înregistrarea + Vă rugăm să vă conectați cu cardul de sănătate și să salvați detaliile de conectare cu datele biometrice pentru a vedea dispozitivele conectate. + %1$s x dimineața + %1$s x Prânz + %1$s x seara + %1$s x Noaptea + Cu excepția cazului în care medicul dumneavoastră v-a dat instrucțiuni diferite, instrucțiunile de utilizare pot fi înțelese după cum urmează: + Medicul dumneavoastră v-a dat aceste informații despre luarea medicamentului. + Nu există informații despre cum să luați medicamentul din rețetă. + Medicul dumneavoastră a observat că vi s-au dat instrucțiuni despre cum să luați acest medicament care nu este pe rețetă. Acest lucru ar putea fi în planul dumneavoastră de medicamente, de exemplu. + Nu este specificat + DJ + Instructiuni de utilizare + " %s - %s" + Memento de venit + Luați mementouri + Medicamente orale + Despre mementourile de admisie + Dimineaţă + Amiază + după-amiază + Seara + droguri + Informații despre dozare + 1 + %s / %s + 1 doza + Schimbați doza + Anulare + Memento de medicamente + Programul de medicație + Luați doza conform prescripției medicului + La ora %s luați %s x + Selectați datele + Mai departe + Anula + O + Din + Urmărire + Nelimitat + Individual + Prima zi + Ultima zi + Adăugați timp + Fără mementouri de admisie + Puteți seta mementouri pentru rețetele dvs + Pornit + Oprit + Ține-mă minte + Dezactivați optimizarea bateriei pentru această aplicație. + Dacă nu activați această opțiune, mementourile privind medicamentele pot părea nesigure. + Anulare + Permite + Instructiuni de utilizare + Nu este specificat + informaţii + De asemenea, amintiți-vă în modul de optimizare a bateriei + Schimbați doza + Mulţimea + formă + încheiat + nelimitat + la %s + Repeta %s + Acolo unde este posibil, folosim informațiile stocate în prescripție pentru calcul. + Anulare + Salva + Până la sfârșitul pachetului + Timp + doza + Vedeți medicamentele + Ți-ai luat deja medicamentele? + Luați mementouri + Fără amintiri active + Nu aveți mementouri de luat astăzi + Selectați data de începere + Selectați data de încheiere + Bun venit + în aplicația de e-rețetă + Încearcă din nou + Introduceți parola + Vă rugăm să vă asigurați că orice persoană cu care puteți partaja acest dispozitiv și care vă cunoaște informațiile de conectare are acces la rețetele dvs. + Introduceți parola + Introduceți parola pentru a debloca aplicația. + parolă + Parolă + Vă rugăm să introduceți o parolă pentru a securiza aplicația. + Afișați parola + Repetați parola + Ați uitat parola? Vă rugăm să ștergeți aplicația și apoi să o reinstalați. Puteți afla de ce în %s. + Introduceți parola + Parola trebuie să aibă cel puțin opt caractere + Puterea parolei nu este suficientă + Puterea parolei este suficientă + Nu este posibilă copierea de rezervă a dispozitivului + Vă rugăm să configurați mai întâi o copie de rezervă a dispozitivului. + Anula + Setări + Puterea parolei foarte bună + Securitatea aplicației + Backup pentru dispozitiv + Vă rugăm să alegeți cel puțin o metodă pentru a face backup pentru aplicație. + parolă + Schimbaţi parola + Pentru a șterge, trebuie stabilită o conexiune la Prescription Server + Marfa este gata + Mărfurile sunt gata de la %s + Marfa a fost gata chiar acum + Bunurile au fost gata de %s minute + Mărfurile sunt gata de la %s + Dezactivat deoarece nu a fost introdusă nicio parolă. + Dezactivat deoarece parola este prea slabă. + Dezactivat deoarece parolele nu se potrivesc. + Explora + Registrul donărilor de organe + Deschideți registrul donării de organe? + Veți fi redirecționat către registrul donatorilor de organe. Pentru a vizualiza și modifica datele despre donarea de organe, trebuie să vă conectați acolo. + Deschide + Anula + Funcția nu este activă în modul demo diff --git a/app/features/src/main/res/values-ru/strings.xml b/app/features/src/main/res/values-ru/strings.xml index 721641a5..731c5175 100644 --- a/app/features/src/main/res/values-ru/strings.xml +++ b/app/features/src/main/res/values-ru/strings.xml @@ -28,7 +28,7 @@ Не прерывать Вперед Вам потребуется: - Введите номер доступа к карте + Введите номер доступа Ввести PIN-код Попробовать снова Не удалось подключиться к серверу. @@ -59,26 +59,23 @@ Выходные данные Издатель gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Управляющий директор: Доктор. Флориан Хартге\n Регистрационный суд: Окружной суд Берлин-Шарлоттенбург\n Номер коммерческого реестра: HRB 96351.\n Идентификационный номер плательщика НДС: DE241843684. + Руководство: Др. Флориан Фурманн, Бреня Аджей, Dr. Флориан Хартге\nРегистрационный суд: Окружной суд Берлина-Шарлоттенбурга\nНомер коммерческого регистра: HRB 96351\nИдентификационный номер плательщика НДС: DE241843684 Ответственный за содержание - Доктор Флориан Хартге + Доктор Флориан Фурманн, Бреня Аджей, Dr. Флориан Хартге Контактная информация Указание Мы стремимся использовать язык гендерного равенства. Если вы заметите какие-либо ошибки, мы будем рады получить от вас письмо по электронной почте. Современная немецкая платформа цифровой медицины Написать электронное письмо Открыть сайт - Добро пожаловать - Начать процедуру входа Разблокировать регистр Отмена - Безопасность Юридическая информация Выходные данные Защита данных Условия использования - Подробности + Подробности рецепта Отметить как выкупленный Отметить как не выкупленный Форма выпуска @@ -100,8 +97,6 @@ Номер учреждения Номер телефона Адрес электронной почты - Производственная травма - Дата происшествия Номер предприятия, на котором произошел несчастный случай, или номер работодателя Вы хотите навсегда удалить этот рецепт? Удалить @@ -124,7 +119,6 @@ Открыть сканер рецептов Настройки Отключить скриншоты - Скрывать предпросмотр при смене приложения Разрешаете ли вы E-Prescription анонимно анализировать ваше поведение при использовании? Техническая информация Защита данных ваших рецептов @@ -146,23 +140,15 @@ Анонимный анализ остается деактивирован %s Спасибо за вашу поддержку! регистр - Пожалуйста, назовите себя, чтобы скачать рецепты. - Примечание для аптек: Контактные данные и информацию об аптеках мы получаем на сайте mein-apothekenportal.de Немецкой ассоциации аптек. Вы обнаружили ошибку или хотите исправить данные? + Пожалуйста, назовите себя. + Примечание для аптек: контактные данные и информацию об аптеках мы получаем на сайте mein-apothekenportal.de. Вы обнаружили ошибку или хотите исправить данные? Узнать больше - аптеки + Aптеки К сожалению, выполнить не удалось \uD83D\uDE15 Пожалуйста, попробуйте еще раз. - Ввести пароль - Далее Вспомогательные инструменты - Изменение масштаба - Изменение размеров содержимого в окне приложения сведением или разведением пальцев на экране. - Пароль - Защитите свои данные, установив собственный пароль. - Пароль + Включить масштабирование Сохранить - Показать пароль - Повторить пароль Рекомендации: %s Написать электронное письмо При отправке сообщения будет передана следующая информация об используемом аппаратном обеспечении и операционной системе: @@ -173,7 +159,7 @@ Отправка Фильтр Фильтры - Местоположение недоступно + Поделиться местоположением Понятно Пароли совпадают Ошибка 20 10 76631 @@ -185,8 +171,6 @@ Обнаружено %s неудачных попыток входа в систему. - Выбрать наилучшую функцию защиты устройства - Это может быть отпечаток пальца, графический ключ и т.п. Токены Токен доступа SSO-токен @@ -194,7 +178,7 @@ SSO-токен недоступен копирование в буфер обмена выполнено Нажмите, чтобы скопировать токен в буфер обмена - Действует только сегодня + Возможен обмен только сегодня Разрешить Отсутствует соединение с сервером Попробуйте снова через несколько минут @@ -262,7 +246,7 @@ %s новых рецептов Можно выкупить - В искуплении + находится в обработке Выкуплен Неизвестно Показать протоколы доступа @@ -274,7 +258,7 @@ Рецепт в настоящее время обрабатывается и не может быть удален Принимать Видимо, что-то пошло не так - Мы знаем, что подключение к медицинской карте имеет свои подводные камни. В будущем регистрация также станет возможной через уже проверенное приложение медицинского страхования. \n\n Мы также работаем над тем, чтобы рецепты можно было выкупать в цифровом виде без регистрации. \n\n Заметили ли вы что-нибудь во время этого процесса, чем хотели бы поделиться с нами? Напишите нам, мы также будем рады получить критический отзыв. + Мы знаем, что подключение к медицинской карте имеет свои подводные камни. В будущем регистрация также станет возможной через уже проверенное приложение медицинского страхования.\n\nМы также работаем над тем, чтобы рецепты можно было выкупать в цифровом виде без регистрации.\n\nЗаметили ли вы в ходе этого процесса что-нибудь, чем хотели бы с нами поделиться? Напишите нам, мы также будем рады получить критический отзыв. Советы по улучшению качества соединения Улучшите качество соединения Снимите чехол (при наличии). @@ -297,7 +281,7 @@ Отсканирован %s Отмечен как выкупленный %s Как вы хотели бы продолжить? - Заказать + Отправить в аптеку Будет доступно в ближайшее время Забронируйте сейчас для самовывоза или закажите доставку курьерской службой или доставку Сохранить, чтобы заказать позднее @@ -329,13 +313,11 @@ https://www.openstreetmap.org/copyright Конфиденциальность и использование Далее - Вы получили PIN-код своей медицинской карты от своей медицинской страховой компании с использованием безопасной процедуры, такой как Postident. + Вам приходилось активно заказывать PIN-код своей медицинской карты в своей медицинской страховой компании, а затем получать его через безопасный процесс, такой как Postident. PIN-код не получен PIN-код Проверьте соединение с Интернетом и настройки времени/даты на вашем устройстве. - Для входа нажмите «Разблокировать». Блокировка? Проверьте свои биометрические данные доступа на этом устройстве. - Забыли пароль? Удалите приложение и затем установите его заново. Причины мы объясняем в %s. Раздел справки Размер упаковки и единица измерения Действующее вещество @@ -348,10 +330,6 @@ Отменить Указание Помогите нам сделать это приложение лучше - Ввести пароль - Пароль должен состоять как минимум из восьми символов - Надежность пароля недостаточная - Надежность пароля достаточная Показывать пароль Не показывать пароль Биометрия @@ -458,9 +436,6 @@ Отправлено только что Отправлено в %s Недействительно - Войти с помощью приложения - Выбрать страховую организацию - Не нашли то, что искали? Этот список постоянно расширяется. Регистрация с помощью медицинской карты уже поддерживается каждой медицинской страховой компанией. Обратная связь от приложения E-Rezept Мы будем рады вашим откликам. Введите в поле, расположенное ниже, максимально точные формулировки: PUK-код @@ -474,13 +449,13 @@ Указание В целях безопасности соединение с сервером рецептов прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. Настроить биометрическую защиту - Сохранение данных доступа невозможно. Сначала настройте на своем устройстве биометрическую защиту (например, с помощью отпечатка пальца). + Невозможно сохранить данные доступа. Предварительно настройте на своем устройстве биометрическую безопасность (например, отпечаток пальца). Отмена Настройки Указание Принимать Защита данных ваших рецептов - \"Это приложение использует самый надежный биометрический датчик, имеющийся на вашем устройстве, для обеспечения безопасности ваших данных доступа в защищенном разделе памяти устройства. \" + \Это приложение использует самый безопасный биометрический датчик вашего устройства для защиты ваших учетных данных в защищенной области памяти устройства.\ Биометрическая защита ваших данных доступа позволяет в будущем без помощи медицинской карточки и ввода PIN-кода открывать это приложение, а также просматривать, запрашивать, выкупать и удалять рецепты. Примите во внимание: если этим устройством вместе с вами пользуются другие люди, которые сохранили свои биометрические параметры на устройстве, они могут получить доступ к вашим рецептам. К сожалению, выполнить не удалось @@ -493,7 +468,7 @@ Фамилия Страховая организация Номер застрахованного лица - Номер доступа к карте + Номер доступа регистр Выйти Сохранить @@ -509,8 +484,7 @@ соединение потеряно Подключиться к серверу рецептов сейчас? Нет жетонов - Вы получите токен, когда войдете в систему рецептурной службы.\n - заказы + Вы получите токен, когда войдете в службу рецептов. Выберите нужный PIN-код разблокировать карту Выберите PIN-код @@ -527,26 +501,26 @@ Получить код самовывоза Сообщение не может быть отображено Пожалуйста, свяжитесь с вашей аптекой ( %s ). - Показать ссылку на корзину + Ссылка на аптеку Показать код самовывоза Показать сообщение %s в %s часов - Рецепт отправлен %s . Процесс цифрового погашения все еще незнаком для многих аптек. Если вы не ответите завтра, мы рекомендуем позвонить и узнать в качестве меры предосторожности. + Рецепт отправлен %s . В некоторых аптеках пока нет возможности цифрового ответа. Если до завтра не будет ответа, пожалуйста, позвоните в качестве меры предосторожности. Обзор заказа Новый Курс Заказ - Бесплатно для звонящего. Время обслуживания: пн-пт с 8:00 до 20:00, кроме национальных праздников. + Бесплатно для звонящего. График работы: пн-пт с 8:00 до 20:00, кроме государственных праздников. Аптека Выберите нужный PIN-код Желаемый PIN-код сохранен Сейчас открыто и рядом со мной Сортировать по … - начать поиск + Искать Прямое назначение аптеки Номер телефона (необязательно) - Поиск по имени или адресу + Искать Нет достоверной информации об аптеке. Никакой актуальной информации об этой аптеке не найдено. Запись об этой аптеке будет удалена. OK @@ -566,16 +540,15 @@ Улучшения продукта Анонимный анализ Помогите нам сделать это приложение лучше. Все данные об использовании собираются анонимно и служат исключительно для улучшения пользовательского опыта. - Безопасность приложений персональные настройки Вспомогательные инструменты Улучшения продукта Добавлен рецепт Рецепт уже доступен Произошла ошибка при импорте + Номер доступа Удалить Отсканированный рецепт - Возможна замена препарата Забыт PIN-код %s Рецепт @@ -593,7 +566,7 @@ Вы можете в любое время изменить это решение в системных настройках. Продолжить Принимать - Это приложение использует самый безопасный метод, предусмотренный вашим устройством. + Приложение использует самый безопасный метод, который вы установили на своем устройстве. Сохранить Выбирать Препарат @@ -621,15 +594,13 @@ Что такое прямое задание? При прямом направлении рецепт из практики или больницы выписывается непосредственно в аптеке. Застрахованные лица не обязаны предпринимать никаких действий и не могут вмешиваться в процесс погашения. \n\n Прямые направления указаны в приложении электронного рецепта, чтобы сделать ваше лечение более прозрачным для вас. Плата за аварийное обслуживание - Иногда возникает необходимость в спешке. Некоторые рецепты могут быть заполнены без дополнительной оплаты за услуги неотложной помощи, например, в ночное время или в праздничные дни. + Если рецепт выписан с 20:00 до 6:00 или по воскресеньям и праздничным дням, может взиматься дополнительная плата в размере 2,50 евро. Лекарства, подлежащие доплате Освобождены от дополнительной оплаты - Те, у кого есть государственная медицинская страховка, должны заплатить дополнительную плату в размере до десяти евро за лекарства, отпускаемые по рецепту. \n\n Размер доплаты зависит от цены вашего лекарства. За лекарства стоимостью менее 5 евро вам придется платить самостоятельно.\n За более дорогие лекарства придется заплатить десять процентов от цены, но минимум 5 евро и максимум 10 евро. \n\n Дети и молодые люди до 18 лет, как правило, освобождаются от дополнительной оплаты. \n\n Если ваши годовые расходы на лекарства превышают лимит вашего финансового бремени, вы можете быть освобождены от доплаты. Поговорите об этом со своей медицинской страховой компанией. + Люди с обязательной медицинской страховкой обычно платят максимум 10 евро за рецептурные лекарства. Более высокие сборы могут взиматься, если запрашивается препарат от конкретного производителя, который не покрывается соглашением о скидках медицинского страхования («запрашиваемый препарат»). \n\n Дети и молодые люди до 18 лет освобождаются от дополнительных выплат. \n\n Если рецепты выкупаются позднее, чем через 28 дней после их выдачи, расходы должны быть оплачены в полном объеме. \n\n Если вы потратите много лекарств в течение года, вы можете подать заявление на освобождение от доплаты в своей медицинской страховой компании. Вы освобождаетесь от доплаты за этот препарат. Ваша медицинская страховая компания покроет стоимость лекарства. Как долго действителен этот рецепт? В течение этого периода вы можете выкупить рецепт в любой аптеке с максимальной доплатой в размере 10 евро. - Возможна замена препарата - В соответствии с юридическими требованиями вашей медицинской страховой компании вам может быть предоставлена ​​альтернатива с тем же активным ингредиентом. \n\n Лекарства могут выглядеть и называться по-разному, иметь разные цены и производителей, но при этом содержать одно и то же действующее вещество. Сам активный ингредиент и дозировка имеют решающее значение для воздействия лекарства на организм. Пациенты часто получают в аптеке лекарство, отличное от того, которое прописал врач, при условии, что лекарство сопоставимо. Для изменения могут быть терапевтические и экономические причины. Отсканированный рецепт Рецепты, импортированные из печатной копии, не могут отображать личную или медицинскую информацию по соображениям безопасности. \n\n Войдите в это приложение с помощью карты здоровья или страховки, чтобы просмотреть всю информацию, содержащуюся в рецепте. Рецепт неправильный @@ -639,7 +610,7 @@ Телефон Веб-сайт Электронная почта - Сортировка по расстоянию невозможна. + Поделитесь своим местоположением, чтобы найти ближайшие к вам аптеки. OK Введите текущий PIN-код Введен неверный PIN-код @@ -657,12 +628,10 @@ страховой полис Биометрия Вход не выполнен - Нам интересно ваше мнение. Пожалуйста, уделите пять минут, чтобы заполнить наш опрос. Заранее большое спасибо. Предупреждение Аптека добавлена ​​в избранное Аптека удалена из избранного Мои аптеки - Надежность пароля очень высокая Операция записи не удалась PIN-код не удалось сохранить. Сообщить @@ -681,7 +650,6 @@ Погашено %s Погашен только сейчас Погашено в %s часов - заказы Этот рецепт был выкуплен для вас в рамках лечения. Плата за аварийное обслуживание Этот рецепт невозможно получить в аптеке в ночное время без дополнительной оплаты сбора за неотложную помощь. @@ -695,7 +663,7 @@ Получать рецепты в цифровом формате? Потяните экран вниз, чтобы обновить. Нет рецептов - Войдите в систему, чтобы получать рецепты автоматически, или добавьте новый рецепт, используя ⊕ в верхнем углу. + Зарегистрируйтесь, чтобы получать рецепты автоматически. регистр Архив рецептов Может быть позже @@ -732,9 +700,9 @@ Нажмите на дисплей, чтобы пропустить появившуюся всплывающую подсказку. Как выкупить? Как бы вы хотели получить лекарство? - Погасить напрямую - Получите лекарство на месте - Заказать + Показать код + Отсканируйте его в аптеке. + Отправить в аптеку Зарезервируйте или закажите доставку Готово Код коллекции @@ -802,7 +770,7 @@ Это приведет к удалению всех квитанций о расходах с этого устройства и сервера. Получайте квитанции о расходах Ваши квитанции о расходах также сохраняются на сервере рецептов. - Полученный + Активировать Итого: %s %s Выберите Расколоть @@ -851,17 +819,17 @@ Требуется соответствующий PIN-код Можно использовать только завтра как самостоятельный плательщик Осталось всего %s дней, чтобы использовать средства самостоятельной оплаты. - \nПо-прежнему можно погасить в качестве самостоятельного плательщика в течение %s дн.\n - Действительно только в течение %s дней - \nДействует еще %s дн.\n - Действует только завтра + По-прежнему можно погасить в качестве самостоятельного плательщика в течение %s дн. + Для активации осталось всего %s дн. + Можно погасить в течение %s дн. + Возврат возможен только завтра Взимается дополнительная плата. Берет страховку Рецепт(ы) успешно перенесены. Рецепт не может быть обработан. Пожалуйста, попробуйте еще раз. Возможно, вам придется выбрать другую аптеку. Рецепт не может быть обработан. Аптека сообщает о неизвестной ошибке. При необходимости обратитесь в другую аптеку. Рецепт был отклонен аптекой. Рецепт может быть недействительным, либо ваш адрес доставки или контактная информация могут быть недействительными. - Не удалось активировать, проверьте подключение к Интернету. + Попробуйте еще раз и, возможно, выберите другую аптеку. Если ошибка не исчезнет, обратитесь в службу поддержки. Рецепт успешно передан. Однако аптека сообщает об ошибке обработки. Пожалуйста, обратитесь в аптеку. Рецепт был отклонен аптекой. Рецепт уже погашен. Рецепт был отклонен аптекой. Рецепт удален. @@ -894,7 +862,6 @@ Активировать демонстрационный режим Не сейчас Поиск - Требуется смартфон с поддержкой NFC Получайте квитанции о расходах в цифровом виде После активации квитанций о расходах вы найдете их здесь после погашения рецепта. Активировать @@ -902,11 +869,11 @@ Как только аптека внесет квитанцию, она появится здесь. Квитанция о стоимости Получайте квитанции о расходах в цифровом виде - Примечание. Вы больше не будете получать квитанции о расходах в распечатанном виде в\n Аптека.\n + Примечание. Вы больше не будете получать квитанции о расходах в распечатанном виде в аптеке. Активировать Может быть позже соглашение - С вашего согласия вы воздержитесь от его распечатки в аптеке.\n и в будущем отправляйте квитанции о расходах в цифровом виде.\n + С вашего согласия вы воздержитесь от их распечатки в аптеке и в будущем будете предоставлять квитанции о расходах в цифровом виде. Отмена Соглашаюсь Выйти из демонстрационного режима @@ -921,17 +888,17 @@ Без интернета Нет подключения к Интернету. Неправильный запрос - Возникла проблема с запросом. Мы работаем над решением.\n + Возникла проблема с запросом. Мы работаем над решением. Сервер не отвечает Попробуйте снова через несколько минут. Не удалось подключиться - Никакую информацию в настоящее время невозможно получить. Пожалуйста\n Попробуйте позже.\n + Никакую информацию в настоящее время невозможно получить. Пожалуйста, повторите попытку позже. Отклоненный - Сервер отклонил ваш запрос. Пожалуйста, попробуйте\n снова позже.\n + Сервер отклонил ваш запрос. Пожалуйста, повторите попытку позже. Обновить приложение - Чтобы использовать эту функцию, обновите приложение.\n + Чтобы использовать эту функцию, обновите приложение. Вход в систему не выполнен - На сервере возникла проблема со входом. Пожалуйста, свяжитесь с нами\n снова.\n + На сервере возникла проблема со входом. Пожалуйста, войдите снова. регистр Копировать URL Подключитесь к внешнему приложению медицинского страхования. @@ -957,12 +924,287 @@ рекомендуемые Цифровой идентификатор здоровья Изменить настройки? - 404 страница? Пожалуйста, включите открытие ссылок в настройках, чтобы продолжить. Открыть настройки Устаревшая версия приложения Ваша версия приложения устарела и больше не поддерживается. Чтобы продолжить использование электронного рецепта, обновите приложение. Обновить - Ваш CAN имеет длину %s цифр. + Ваш номер доступа состоит из %s цифр. Ваш PIN-код может содержать от %s до %s цифр. + Подобрать + Курьер + Отгрузка + Требуется смартфон с поддержкой NFC + Нам не удалось обработать запрос на удаление рецепта. Мы работаем над решением ( %s ). + При удалении рецепта что-то пошло не так, я получил код ошибки %s . Пожалуйста, сообщите мне, когда удаление снова станет возможным. + Далее + Неправильный запрос + Сообщить + Удалить локально + Без интернета + Рецепт не удалось удалить. Пожалуйста, проверьте подключение к Интернету и повторите попытку. + Попробовать снова + OK + Повторный вход + Вы должны войти в систему, чтобы удалить рецепт (401). + регистр + Отмена + Удаление невозможно + В настоящее время вам не разрешено удалять рецепт, поскольку он находится в аптеке и подлежит выписке или ожидает прямого назначения (403). + Слишком много запросов + Вы пытались удалить рецепт слишком много раз. Пожалуйста, повторите попытку позже (429). + Удалить рецепт + Упс... + «Произошла непредвиденная ошибка сервера. Повторите попытку позже (500)». + Безопасность приложения невозможна. Предварительно настройте на своем устройстве биометрическую безопасность (например, отпечаток пальца). + Биометрическое резервное копирование невозможно для этого устройства, поскольку оно не поддерживается вашим устройством. + Опасность! Ваш телефон недостаточно защищен. + Данные доступа сохраняются на вашем телефоне. Для защиты доступа используется предпочитаемый вами способ входа в систему. Этот способ входа, например, перелистывание шаблона, не очень безопасен. Вы используете функцию \"Сохранить данные для входа\" на свой страх и риск. + Если вы по-прежнему используете функцию «Сохранить данные для входа», в будущем вы сможете просматривать, получать доступ, использовать или удалять рецепты с помощью приложения электронного рецепта без карты здоровья и ввода PIN-кода. + Местоположение не найдено + Отправить в аптеку + Показать код + Язык + Язык + Немецкий + Aрабский + Болгарский + Чешский + Датский + Английский + Французский + Иврит + Итальянский + Голландский + Польский + Румынский + Русский + Турецкий + Украинец + Стандарт + Цифровые квитанции о расходах были деактивированы и удалены. + «Форум электронных рецептов» + Никакая альтернативная подготовка невозможна + Возможна замена препарата + Подготовка замены (Aut idem) + Фармацевты обязаны отдавать приоритет отпуску лекарств, на которые медицинская страховая компания пациента заключила с производителями лекарств соглашение о скидках. Это не применяется только в том случае, если врач исключает из рецепта «Aut idem», чего нет в вашем рецепте. + Ваш врач определил, что вам следует принимать назначенный препарат. Аптека не должна осуществлять обмен на основании договора о скидке («Aut idem»). + Разрешить снимки экрана + Если вы разрешите создание снимков экрана, последняя открытая вами страница останется видимой в фоновом режиме при переключении приложений. При необходимости ваши персональные данные могут быть видны. Поэтому мы рекомендуем не разрешать создание снимков экрана. + Разрешить + Отмена + Переключите клавиатуру на стикеры или используйте смайлы для изображения профиля. + Выберите изображение профиля + Как вы хотели бы продолжить? + Выбрать фото + камера + Эмодзи + Отмена + Открыть настройки + Использовать + Сделайте фото + Редактировать изображение профиля + Предполагалось %s минут назад + Принято %s + Только что принято + Принято в %s часов + HealthID + Выбрать страховую организацию + Если регистрация с помощью Health ID прошла не так, как ожидалось, следуйте советам нашего справочного центра. + Справка + Справка + Советы по регистрации в страховом приложении + Ваша страховая компания несет ответственность за Health ID. Пожалуйста, свяжитесь с ними, если у вас есть какие-либо вопросы по поводу регистрации. Вот несколько проверенных советов: + Обратите внимание, что в зависимости от вашей страховки может потребоваться отдельное приложение. Пожалуйста, обратитесь в свою страховую компанию, чтобы узнать, что это такое. + Запустите приложение кассового аппарата и войдите в него один раз, прежде чем приступить к регистрации в приложении электронного рецепта. + Переключение между регистрацией с помощью Health ID и карты здоровья может привести к проблемам. Поэтому, пожалуйста, сначала активно выйдите из своего профиля, прежде чем менять вариант входа. + Если вашей страховки нет в списке, вы также можете зарегистрироваться, используя свою медицинскую карту и соответствующий PIN-код. + Если приложение кассового аппарата не перенаправляет вас обратно в приложение электронного рецепта, обязательно сообщите об этой ошибке в свою страховую компанию. + Если доступ к приложению страхования невозможен, возможно, будет полезно зайти в настройки браузера вашего смартфона и разрешить открытие ссылок. + Если ваш смартфон работает под управлением Android 14, возможно, было бы полезно разрешить открытие ссылок в настройках. + Открыть настройки + К сожалению, выполнить не удалось + Повторите попытку позже. + Сообщения об ошибках при загрузке. + Пожалуйста, выберите смайлик или текст + Сохранить + Пожалуйста, войдите, чтобы продолжить + Далее + Вызов + Писать\nПочта + В аптеку + Аптека + Сегодня + Завтра + Маршрут\nздесь + Удаление рецепта и квитанции о расходах + Если вы удалите рецепт, связанный с ним приход затрат также будет удален. + Сообщения из всех профилей теперь отображаются вместе. + Новые возможности + Новые возможности + Мы постоянно работаем над новыми функциями. Поделитесь с нами своим мнением и помогите нам создать лучшее приложение. + Заказы для всех + В разделе «Заказы» теперь вы можете увидеть заказы для всех профилей вместе, и просматривать их стало гораздо проще. + + Без возмещения затрат + Как правило, медицинская страховка не покрывает расходы на этот рецепт. Таким образом, как пациент, вы несете ответственность за полную оплату. Будут ли возмещены расходы в рамках дополнительного страхования или предусмотренных законом льгот, необходимо проверять индивидуально. + Ваша страховка не покрывает никаких расходов на этот рецепт. + + «Ваша страховка не покрывает рецепт %s » + + + «Ваша страховка не покрывает рецепты %s ». + + Вызванный + Дата + Несчастный случай + Производственная травма + Промышленная болезнь + Новости + Нет новостей + У вас пока нет сообщений. + 🎉Ваш заказ готов к самовывозу. Пожалуйста, покажите этот код получения, чтобы идентифицировать себя. + К сожалению, сообщение из вашей аптеки было пустым. Обратитесь в свою аптеку. + В аптеке вам дали ссылку. + Новости + Заказ + Сообщения + Нет подключения к Интернету + Для просмотра подключенных устройств необходимо подключение к биометрии. + Рецепт удален %s + Удалено + Нет рецептов + У вас нет рецептов в архиве. + Ваши квитанции о расходах были удалены. + Квитанция о стоимости + Требуется регистрация + Пожалуйста, войдите в систему, чтобы просмотреть подключенные устройства. + регистр + регистр + Подключиться к страховке %s + Карта здоровья на смартфоне + Вход не выполнен + Зарегистрирован + обновить + Вы получите протоколы доступа, когда войдете в службу рецептов. + Требуется регистрация + Пожалуйста, войдите в систему, используя свою медицинскую карту, и сохраните свои данные для входа с биометрическими данными, чтобы просмотреть подключенные устройства. + %1$s x утро + %1$s x обед + %1$s x вечер + %1$s x Ночью + Если ваш врач не дал вам иных указаний, инструкцию по применению можно понимать следующим образом: + Ваш врач предоставил вам эту информацию о приеме лекарства. + В рецепте нет информации о том, как принимать лекарство. + Ваш врач отметил, что вам были даны инструкции о том, как принимать это лекарство, которого нет в рецепте. Это может быть, например, в вашем плане лечения. + Нет данных + диджей + Инструкция по применению + " %s - %s" + Напоминание о доходах + Принимайте напоминания + Пероральные препараты + О напоминаниях о приеме + Утро + Полдень + Полдень + Вечером + Препарат + Информация о дозировке + 1 + %s / %s + 1 доза + Изменить дозировку + Отмена + Напоминание о приеме лекарств + График приема лекарств + Принимайте дозировку по назначению врача. + В %s час возьмите %s x + Выберите даты + Далее + Отмена + А + Из + Отслеживание + Безлимитный + Индивидуально + Первый день + Последний день + Добавить время + Нет напоминаний о приеме + Вы можете установить напоминания о своих рецептах. + Вкл. + Выкл. + Запомнить меня + Отключите оптимизацию заряда батареи для этого приложения. + Если вы не активируете эту опцию, напоминания о лекарствах могут оказаться ненадежными. + Отмена + Разрешить + Инструкция по применению + Нет данных + информация + Также помните, что в режиме оптимизации батареи + Изменить дозировку + Количество + форма + закончился + неограниченный + до %s + Повторить %s + По возможности мы используем для расчета информацию, хранящуюся в рецепте. + Отмена + Сохранить + До конца пакета + Время + доза + Посмотреть лекарства + Вы уже приняли лекарство? + Принимайте напоминания + Нет активных воспоминаний + У вас нет напоминаний, которые можно было бы сделать сегодня. + Выберите дату начала + Выберите дату окончания + Добро пожаловать + в вашем приложении для электронных рецептов + Попробуйте еще раз + Введите пароль + Убедитесь, что все, с кем вы можете поделиться этим устройством и кто знает ваши данные для входа, также имеют доступ к вашим рецептам. + Введите пароль + Пожалуйста, введите пароль, чтобы разблокировать приложение. + пароль + пароль + Пожалуйста, введите пароль, чтобы защитить приложение. + Показать пароль + Повторите пароль + Забыли пароль? Пожалуйста, удалите приложение, а затем переустановите его. Вы можете узнать почему в нашем %s. + Введите пароль + Пароль должен иметь длину не менее восьми символов + Недостаточная надежность пароля + Достаточно надежность пароля + Резервное копирование устройства невозможно. + Сначала настройте резервное копирование устройства. + Отмена + Настройки + Надежность пароля очень хорошая + Безопасность приложений + Резервное копирование устройства + Пожалуйста, выберите хотя бы один способ резервного копирования приложения. + пароль + Изменить пароль + Для удаления необходимо установить соединение с Prescription Server. + Товары готовы + Товары готовы с %s + Товары были готовы только сейчас + Товары готовы уже %s минут + Товары готовы с %s + Отключено, поскольку пароль не введен. + Отключено, поскольку пароль слишком слабый. + Отключено, поскольку пароли не совпадают. + Исследовать + Реестр донорства органов + Открыть реестр доноров органов? + Вы будете перенаправлены в реестр доноров органов. Чтобы просмотреть и изменить данные о донорстве органов, вам необходимо войти туда. + Открыть + Отмена + Функция не активна в демонстрационном режиме diff --git a/app/features/src/main/res/values-tr/strings.xml b/app/features/src/main/res/values-tr/strings.xml index 03ddce30..fe949d62 100644 --- a/app/features/src/main/res/values-tr/strings.xml +++ b/app/features/src/main/res/values-tr/strings.xml @@ -26,7 +26,7 @@ İptal etme Hadi gidelim Şunlara ihtiyacınız var: - Kart erişim numarasını girin + Erişim numarasını girin PIN girin Tekrar dene Sunucu bağlantısı başarısız oldu. @@ -55,26 +55,23 @@ Künye Editör gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Genel Müdür: Dr. Florian Hartge\n Kayıt mahkemesi: Berlin-Charlottenburg Bölge Mahkemesi\n Ticari sicil numarası: HRB 96351\n KDV kimlik numarası: DE241843684 + Yönetim: Doç. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nKayıt mahkemesi: Berlin-Charlottenburg Bölge Mahkemesi\nTicaret sicil numarası: HRB 96351\nKDV kimlik numarası: DE241843684 İçerikten sorumlu - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge İletişim Not Cinsiyet eşitliğine uygun bir dil kullanmaya çalışıyoruz. Herhangi bir hata fark ederseniz, sizden e-posta ile haber almaktan memnuniyet duyarız. Almanya\'nın dijital tıp için modern platformu E-posta yaz Web sitesini aç - Hoş geldiniz - Oturum açmayı başlat Blokeyi kaldır Giriş yapmak İptal et - Güvenlik Yasal Künye Veri koruma Kullanım şartları - Ayrıntılar + Tarif ayrıntıları Kullanıldı olarak işaretle Kullanılmadı olarak işaretle İlaç türü @@ -96,8 +93,6 @@ Kuruluş numarası Telefon numarası E-posta adresi - İş kazası - Kaza günü Kaza şirketi veya işveren numarası Bu reçeteyi kalıcı olarak silmek ister misiniz? Sil @@ -120,7 +115,6 @@ Reçeteler için tarayıcıyı açın Ayarlar Ekran görüntülerini gizle - Uygulamaları değiştirdiğinizde önizleme görüntüsünün görüntülenmesini engeller E-Reçetenin kullanım davranışınızı anonim olarak analiz etmesine izin veriyor musunuz? Teknik bilgiler Reçete verilerinizin güvenliği @@ -142,23 +136,15 @@ Anonim analiz devre dışı kalıyor %s Desteğiniz için teşekkürler! Giriş yapmak - Reçeteyi indirmek için lütfen kimliğinizi doğrulayın. - Eczaneler için not: Eczanelerin iletişim bilgilerini ve bilgilerini Deutscher Apothekenverband e.V.\'ın mein-apothekenportal.de adresinden alıyoruz. Bir hata mı buldunuz veya verileri düzeltmek mi istiyorsunuz? + Lütfen kendinizi tanıtın. + Eczaneler için not: Eczanelerin iletişim bilgilerini ve bilgilerini mein-apothekenportal.de adresinden alıyoruz. Bir hata mı keşfettiniz veya verileri düzeltmek mi istiyorsunuz? Daha fazla bilgi - eczaneler + Eczaneler Bu maalesef olmadı \uD83D\uDE15 Lütfen tekrar deneyin. - Şifreyi girin - İleri Kullanım yardımı - Yakınlaştırma - Parmaklarınızı bir araya getirmek veya ayırmak uygulamayı büyütmenizi sağlar (yakınlaştırmak için sıkıştırın). - Parola - Verilerinizi kendiniz seçtiğiniz şifre ile koruyun. - Parola + Yakınlaştırmayı etkinleştir Kaydet - Şifreyi göster - Şifreyi tekrarla Öneriler: %s E-posta yaz Mesajınızı gönderdiğinizde, kullanılan donanım ve işletim sistemi ile ilgili aşağıdaki bilgiler iletilecektir: @@ -169,7 +155,7 @@ Kargo Filtre Filtrele - Herhangi bir konum mevcut değil + Konumu paylaş Anladım Tekrarlanan şifre eşleşiyor Hata 20 10 76631 @@ -179,8 +165,6 @@ Başarısız oturum açma denemelerinin %s tespit edildi. %s başarısız oturum açma girişimi algılandı. - En iyi cihaz emniyetini seçin - Bu, bir parmak izi, bir silme deseni veya benzeri olabilir Tokenler Access Token SSO Token @@ -188,7 +172,7 @@ Herhangi bir SSO Token mevcut değil Ara belleğe kopyalandı Token\'i ara belleğe kopyalamak için tıklayın - Yalnızca bugün geçerli + Yalnızca bugün kullanılabilir İzin ver Sunucuya bağlantı yok Lütfen birkaç dakika sonra tekrar deneyin. @@ -254,7 +238,7 @@ %s yeni tarifler Kullanılabilir - kurtuluşta + İşleniyor Kullanıldı Bilinmiyor Erişim protokollerini göster @@ -266,7 +250,7 @@ Reçete şu an düzenlenmekte ve silinemez Kabul Bu maalesef başarılı olmadı - Sağlık kartıyla bağlantının bazı tuzakları olduğunun bilincindeyiz. Gelecekte, kimlik doğrulaması yapılmış bir sağlık sigortası uygulaması aracılığıyla kayıt da mümkün olacaktır. \n\n Reçetelerin kayıt olmadan dijital ortamda kullanılabilmesini sağlamak için de çalışıyoruz. \n\n Bu süreçte bizimle paylaşmak istediğiniz bir şey fark ettiniz mi? Lütfen bize yazın, çok kritik geri bildirimler almaktan da mutluluk duyarız. + Sağlık kartıyla bağlantının bazı tuzakları olduğunun bilincindeyiz. Gelecekte, kimlik doğrulaması yapılmış bir sağlık sigortası uygulaması aracılığıyla kayıt da mümkün olacaktır.\n\nReçetelerin kayıt olmadan dijital olarak kullanılabilmesini sağlamak için de çalışıyoruz.\n\nBu süreçte bizimle paylaşmak istediğiniz bir şey fark ettiniz mi? Lütfen bize yazın, çok kritik geri bildirimler almaktan da mutluluk duyarız. Bağlantı ip uçları Bağlantıyı güçlendirin Gerekirse koruyucu kılıfı çıkarın. @@ -289,7 +273,7 @@ Şu tarihte tarandı: %s Şu tarihte kullanıldı olarak işaretlendi: %s Nasıl devam etmek istiyorsunuz? - Sipariş ver + Eczaneye gönder Yakında kullanıma sunulur Teslim almak için şimdi rezerve edin veya kurye servisi veya nakliye ile teslim ettirin Sonraki siparişler için kaydedin @@ -319,13 +303,11 @@ https://www.openstreetmap.org/copyright Gizlilik ve Kullanım İleri - Sağlık kartınızın PIN kodunu sağlık sigorta şirketinizden Postident gibi güvenli bir prosedür kullanarak aldınız. + Sağlık kartı PIN kodunuzu aktif olarak sağlık sigorta şirketinizden sipariş etmeniz ve ardından Postident gibi güvenli bir süreç aracılığıyla almanız gerekiyordu. PIN alınmadı PIN Lütfen internet bağlantınızı ve cihazınızın saat ile tarih ayarlarını kontrol edin. - Giriş yapmak için “Kilidi Aç”a basın. Engellendiniz mi? Lütfen bu cihazdaki biyometrik erişim verilerinizi tekrar kontrol edin. - Şifreyi mi unuttunuz? Lütfen uygulamayı silin ve ardından yeniden yükleyin. Bunun neden böyle olduğunu şuradan öğrenebilirsiniz: %s. Yardım alanı Paket boyutu ve birimi Etken madde @@ -338,10 +320,6 @@ Geri al Not Bu uygulamayı daha iyi hale getirmemize yardımcı olun - Şifreyi girin - Parola en az sekiz basamaklı olmalıdır - Parola yeterince güçlü değil - Parola yeterince güçlü Şifre görnünür Şifre görünmez Biyometri @@ -446,9 +424,6 @@ Şu anda gönderildi Şu saatte gönderildi: %s Artık geçerli değil - Uygulama ile oturum aç - Sigortayı seç - Aradığınızı bulamadınız mı? Bu liste sürekli genişletilmektedir. Sağlık kartıyla kayıt zaten her sağlık sigortası şirketi tarafından desteklenmektedir. E-Rezept uygulamasından geri bildirimi Geri bildiriminizi bekliyoruz. Lütfen aşağıdaki alanı kullanın ve mümkün olduğunca detaylı yazın: PUK @@ -462,13 +437,13 @@ Not Güvenlik nedeniyle, reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. Biyometrik güvenlik önlemi ayarla - Erişim verilerini kaydetmek mümkün değil. Cihazınızda önceden biyometrik bir güvenlik önlemi (ör. parmak izi) ayarlayın. + Erişim verilerini kaydetmek mümkün değildir. Önceden cihazınızda biyometrik güvenliği (ör. parmak izi) ayarlayın. İptal et Ayarlar Not Kabul Reçete verilerinizin güvenliği - \"Bu uygulama cihazınızın kullanıma sunduğu en güvenli biyometrik sensörünü kullanıyor. Bu şekilde erişim verileriniz cihazınızın hafızasında güvenli bir alanda kaydedilir. \" + \Bu uygulama, kimlik bilgilerinizi cihaz depolama alanının korumalı bir alanında güvence altına almak için cihazınız tarafından sağlanan en güvenli biyometrik sensörü kullanır.\ Erişim verilerinizin biyometrik olarak kaydedilmesi, bu uygulamaya bundan sonra sağlık kartı olmadan ve PIN girmeden giriş yapılmasına, reçetelerin görüntülenmesine, açılmasına, kullanılmasına veya silinmesine olanak sağlar. Lütfen bu cihazı paylaşabileceğiniz ve biyometrik özellikleri bu cihazda saklanabilecek kişilerin de reçetelerinize erişebildiğinden emin olunuz. Bu maalesef olmadı @@ -481,7 +456,7 @@ Ad Sigorta Sigorta numarası - Kart erişim numarası + Erişim numarası Giriş yapmak Oturumu kapat Kaydet @@ -497,8 +472,7 @@ bağlantı koptu Tarif sunucusuna şimdi bağlanılsın mı? Jeton yok - Reçete servisine giriş yaptığınızda bir jeton alacaksınız.\n - Emirler + Reçete servisine giriş yaptığınızda bir jeton alacaksınız. İstediğiniz PIN\'i seçin Kartın blokesini kaldırın PIN\'i seçin @@ -515,26 +489,26 @@ Teslim alma kodu alındı Mesaj gösterilemiyor Lütfen eczanenizle iletişime geçin ( %s ). - Alışveriş sepeti bağlantısını göster + Eczane bağlantısı Teslim alma kodunu göster Mesajı göster %s saat %s s\'de - Tarif %s adresine gönderildi. Dijital ödeme süreci birçok eczane için hâlâ bilinmiyor. Yarına kadar haber alamazsanız, önlem olarak arayıp bilgi almanızı öneririz. + Tarif %s adresine gönderildi. Bazı eczanelerde henüz dijital yanıt seçeneği bulunmamaktadır. Yarına kadar yanıt gelmezse lütfen önlem olarak arayın. Siparişe genel bakış Yeni Kurs Emir - Arayan için ücretsizdir. Hizmet saatleri: Ulusal tatiller hariç Pazartesi - Cuma 08:00 - 20:00 + Arayan için ücretsizdir. Hizmet saatleri: Pazartesi - Cuma 8:00 - 20:00, federal tatiller hariç Eczane İstediğiniz PIN\'i seçin İstenilen PIN kaydedildi Şu anda açık ve yakınımda Tarafından filtre … - Aramaya başla + Aramak Doğrudan atama eczaneler Telefon numarası (isteğe bağlı) - Ada veya adrese göre arayın + Aramak Geçerli eczane bilgisi yok Bu eczane hakkında güncel bilgi bulunamadı. Bu eczanenin girişi silinecek. Tamam @@ -554,16 +528,15 @@ Ürün iyileştirmeleri Anonim analiz Bu uygulamayı daha iyi hale getirmemize yardımcı olun. Tüm kullanım verileri anonim olarak toplanır ve yalnızca kullanıcı deneyimini geliştirmek için kullanılır. - Uygulama güvenliği kişisel ayarlar Kullanım yardımı Ürün iyileştirmeleri Tarif eklendi Tarif zaten mevcut İçe aktarılırken bir hata oluştu + Erişim numarası Sil Taranmış reçete - Muadil mümkün Unutulan PIN %s Tarif @@ -579,7 +552,7 @@ Bu kararı sistem ayarlarında her zaman değiştirebilirsiniz. Devam et Kabul - Bu uygulama, cihazınız tarafından sağlanan en güvenli yöntemi kullanır. + Uygulama, cihazınızda kurduğunuz en güvenli yöntemi kullanır. Kaydet Seçmek ilaç @@ -607,15 +580,13 @@ Doğrudan görev nedir? Doğrudan yönlendirmede, bir muayenehaneden veya hastaneden alınan reçete doğrudan eczanede doldurulur. Sigortalıların herhangi bir işlem yapmasına gerek yoktur ve geri ödeme sürecine müdahale edemezler. \n\n Tedavinizi sizin için daha şeffaf hale getirmek için e-reçete uygulamasında doğrudan yönlendirmeler listelenir. Acil servis ücreti - Bazen acele etmek gerekebilir. Bazı reçeteler, örneğin geceleri veya resmi tatil günlerinde, acil servis ücreti ödenmeden doldurulabilir. + Reçetenin saat 20.00 ile 06.00 arasında veya Pazar ve resmi tatil günlerinde doldurulması durumunda 2,50 Euro ek ücret tahsil edilebilir. Katkı payına tabi ilaçlar Ek ödemeden muaf - Yasal sağlık sigortası olanların reçeteli ilaçlar için on avroya kadar ek ödeme yapması gerekiyor. \n\n Ek ödemenin miktarı ilacınızın fiyatına bağlıdır. Maliyeti 5 €\'dan az olan ilaçları kendiniz ödemek zorundasınız.\n Daha pahalı ilaçlar için fiyatın yüzde onunu ödemeniz gerekir, ancak bu tutar en az 5 Euro, en fazla 10 Euro\'dur. \n\n 18 yaşın altındaki çocuklar ve gençler genel olarak ek ödemeden muaftır. \n\n Yıllık ilaç masraflarınız mali limitinizi aşıyorsa katkı payı ödemesinden muaf olabilirsiniz. Bu konuyu sağlık sigortası şirketinizle görüşün. + Yasal sağlık sigortası olan kişiler genellikle reçeteli ilaçlar için maksimum 10 Euro öderler. Belirli bir üreticiden sağlık sigortası indirim sözleşmesi kapsamına girmeyen bir ilacın talep edilmesi durumunda daha yüksek ücretler geçerli olabilir (\"ilaç talep\"). \n\n 18 yaşın altındaki çocuklar ve gençler ek ödemelerden muaftır. \n\n Reçetelerin, verildikten 28 gün sonra kullanılması durumunda masrafların tamamı karşılanmalıdır. \n\n Yıl içerisinde çok fazla ilaç harcamanız durumunda sağlık sigorta şirketinize katkı payı muafiyeti talebinde bulunabilirsiniz. Bu ilaç için katkı payı ödemekten muafsınız. İlaç masraflarını sağlık sigortanız karşılayacaktır. Bu reçete ne kadar süreyle geçerlidir? Bu süre zarfında reçetenizi herhangi bir eczaneden maksimum 10 € ek ödemeyle alabilirsiniz. - Muadil mümkün - Sağlık sigortanızın yasal gereklilikleri nedeniyle size aynı etken maddeli bir alternatif sunulabilir. \n\n İlaçlar farklı görünebilir ve farklı adlandırılabilir, farklı fiyatlara ve üreticilere sahip olabilir, ancak yine de aynı etken maddeyi içerebilir. İlaçların vücuttaki etkisi açısından etken maddenin kendisi ve dozajı çok önemlidir. İlaçların karşılaştırılabilir olması koşuluyla, hastalar genellikle eczanede doktor tarafından reçete edilenden farklı bir ilaç alırlar. Değişimin tedavi edici ve ekonomik nedenleri olabilir. Taranmış reçete Basılı kopyadan içe aktarılan reçeteler, güvenlik nedeniyle kişisel veya tıbbi bilgileri görüntüleyemez. \n\n Reçetede yer alan tüm bilgileri görüntülemek için bu uygulamaya sağlık kartı veya sigorta uygulamasıyla giriş yapın. Tarif yanlış @@ -625,7 +596,7 @@ telefon İnternet sitesi E-posta - Mesafeye göre sıralama mümkün değil. + Yakınınızdaki eczaneleri bulmak için konumunuzu paylaşın. Tamam Mevcut PIN\'i girin Yanlış PIN girildi @@ -643,12 +614,10 @@ Sigorta kartı Biyometri Oturum açılmamış - Fikrinizle ilgileniyoruz. Lütfen anketimizi tamamlamak için beş dakikanızı ayırın. Şimdiden çok teşekkür ederim. Uyarı notu Eczane favorilere eklendi Eczane favorilerden kaldırıldı Eczanelerim - Şifre gücü çok iyi Yazma işlemi başarılı değil PIN kaydedilemedi Rapor @@ -667,7 +636,6 @@ %s tarihinde kullanıldı Az önce kullanıldı Saat %s de kullanıldı - Emirler Bu reçete, bir tedavinin parçası olarak sizin için kullanıldı. Acil servis ücreti Bu reçete, acil servis ücreti ek olarak ödenmeden gece eczanesinde doldurulamaz. @@ -681,7 +649,7 @@ Reçeteleri dijital olarak mı alıyorsunuz? Yenilemek için ekranı aşağı çekin. Reçete yok - Tarifleri otomatik olarak almak için oturum açın veya üst köşedeki ⊕ işaretini kullanarak yeni bir tarif ekleyin. + Tarifleri otomatik olarak almak için kaydolun. Giriş yapmak Tarif arşivi Belki sonra @@ -718,9 +686,9 @@ Görüntülenen araç ipucunu atlamak için ekrana tıklayın. Nasıl geri alınır? İlaçlarınızı nasıl almak istersiniz? - Doğrudan kullan - İlaçları yerinde kullanın - Sipariş ver + Kodu göster + Eczanede tarattırın + Eczaneye gönder Rezerve edin veya teslim ettirin Hazır Koleksiyon kodu @@ -786,7 +754,7 @@ Bu işlem, bu cihazdaki ve sunucudaki tüm gider makbuzlarını silecektir. Maliyet makbuzlarını alın Maliyet makbuzlarınız da reçete sunucusunda saklanır. - Kabul edilmiş + Etkinleştir Toplam: %s %s Seçmek Bölmek @@ -835,17 +803,17 @@ İlişkili PIN gerekli Kendi kendine ödeme yapan kişi olarak yalnızca yarın kullanılabilir Kendi kendine ödeme yapan kişi olarak kullanmak için yalnızca %s gün kaldı - \nHala %s gün süreyle kendi kendine ödeme yapan kişi olarak kullanılabilir\n - Yalnızca %s gün süreyle geçerlidir - \n%s gün kaldı\n - Sadece yarın geçerli + Hala %s gün süreyle kendi kendine ödeme yapan kişi olarak kullanılabilir + Kullanmak için yalnızca %s gün kaldı + %s gün süreyle hâlâ kullanılabilir + Yalnızca yarın kullanılabilir Ücretler uygulanır Sigorta alır Tarif(ler) başarıyla aktarıldı. Tarif işlenemiyor. Lütfen tekrar deneyin. Farklı bir eczane seçmeniz gerekebilir. Tarif işlenemiyor. Eczane bilinmeyen bir hata bildiriyor. Gerekirse başka bir eczaneyi deneyin. Reçete eczane tarafından reddedildi. Reçete geçersiz olabilir veya teslimat adresiniz veya iletişim bilgileriniz geçersiz olabilir. - Kullanılamadı. Lütfen internet bağlantınızı kontrol edin. + Tekrar deneyin ve muhtemelen farklı bir eczane seçin. Hata devam ederse, lütfen destek ekibiyle iletişime geçin. Tarif başarıyla aktarıldı. Ancak eczane işlem hatası bildiriyor. Lütfen eczaneye başvurun. Reçete eczane tarafından reddedildi. Reçete zaten kullanıldı. Reçete eczane tarafından reddedildi. Tarif silindi. @@ -878,7 +846,6 @@ Demo modunu etkinleştir Şimdi değil Aramak - NFC özellikli akıllı telefon gereklidir Maliyet makbuzlarını dijital olarak alın Maliyet makbuzları etkinleştirildiğinde, reçetenizi kullandıktan sonra bunları burada bulacaksınız. Etkinleştir @@ -886,11 +853,11 @@ Eczane masraf makbuzunu yatırır yatırmaz burada görünecektir. Maliyet makbuzu Maliyet makbuzlarını dijital olarak alın - Not: Artık maliyet makbuzlarınızı çıktı olarak almayacaksınız.\n Eczane.\n + Not: Artık eczaneden maliyet makbuzlarınızı çıktı olarak almayacaksınız. Etkinleştir Belki sonra anlaşma - Rızanızla eczanede basmaktan kaçınacaksınız.\n ve gelecekte maliyet makbuzlarınızı dijital olarak gönderin.\n + Onayınız olması halinde, eczanede çıktı almaktan vazgeçecek ve maliyet makbuzlarınızı gelecekte dijital ortamda sunacaksınız. İptal et Kabul ediyorum Demo modundan çık @@ -905,17 +872,17 @@ İnternet yok İnternet bağlantısı yok. Yanlış istek - İstekle ilgili bir sorun oluştu. Bir çözüm üzerinde çalışıyoruz.\n + İstekle ilgili bir sorun oluştu. Bir çözüm üzerinde çalışıyoruz. sunucu yanıt vermiyor Lütfen birkaç dakika içinde tekrar deneyin. Bağlanma hatası - Şu anda hiçbir bilgi alınamıyor. Lütfen\n daha sonra tekrar deneyin.\n + Şu anda hiçbir bilgi alınamıyor. Lütfen daha sonra tekrar deneyiniz. Reddedilmiş - Sunucu isteğinizi reddetti. Lütfen dene\n sonra tekrar.\n + Sunucu isteğinizi reddetti. Lütfen daha sonra tekrar deneyiniz. Uygulamayı güncelle - Bu özelliği kullanmak için lütfen uygulamanızı güncelleyin.\n + Bu özelliği kullanmak için lütfen uygulamanızı güncelleyin. giriş başarısız oldu - Sunucuya giriş yaparken bir sorun oluştu. Lütfen iletişime geçin\n Tekrar.\n + Sunucuya giriş yaparken bir sorun oluştu. Lütfen tekrar oturum açın. Giriş yapmak Url\'yi kopyala Harici sağlık sigortası uygulamasına bağlanın. @@ -941,12 +908,285 @@ Tavsiye edilen Dijital sağlık kimliği Ayarlar yapılsın mı? - 404 sayfa mı? Devam etmek için lütfen ayarlarınızda bağlantıların açılmasını etkinleştirin. Ayarları aç Eski uygulama sürümü Uygulamanızın sürümü eski ve artık desteklenmiyor. E-reçeteyi kullanmaya devam etmek için lütfen uygulamayı güncelleyin. Güncelle - CAN\'ınız %s basamak uzunluğundadır. + Erişim numaranız %s basamak uzunluğunda. PIN\'iniz %s ila %s basamak uzunluğunda olabilir. + Toplamak + Kurye + gönderi + NFC özellikli akıllı telefon gerekli + Tarifi silme isteğini işleme koyamadık. Bir çözüm üzerinde çalışıyoruz ( %s ). + Reçetemi silerken bir şeyler ters gitti, %s hata kodunu aldım. Silme işlemi tekrar mümkün olduğunda lütfen bana haber verin. + İleri + Yanlış istek + Rapor + Yerel olarak sil + İnternet yok + Tarif silinemedi. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin. + Tekrar dene + Tamam + Tekrar-giriş + Tarifi (401) silmek için giriş yapmalısınız. + Giriş yapmak + İptal et + Silinmesi imkansız + Reçeteyi şu anda silmenize izin verilmiyor çünkü reçete eczanede doldurulmak üzere duruyor veya beklemede olan bir doğrudan görev (403) var. + çok fazla istek + Tarifi silmeyi çok fazla denediniz. Lütfen daha sonra tekrar deneyin (429). + Tarifi sil + Ah... + "Beklenmeyen bir sunucu hatası oluştu. Lütfen daha sonra tekrar deneyin (500)." + Uygulama güvenliği mümkün değil. Önceden cihazınızda biyometrik güvenliği (ör. parmak izi) ayarlayın. + Cihazınız tarafından desteklenmediğinden bu cihaz için biyometrik yedekleme mümkün değildir. + Tehlike! Telefonunuz iyi korunmuyor. + Giriş bilgileriniz telefonunuza kaydedilir. Erişimi korumak için tercih ettiğiniz giriş yöntemi kullanılır. Bu oturum açma yöntemi, örneğin kaydırma modeli, pek güvenli değildir. \"Giriş ayrıntılarını kaydet\" özelliğini kullanmak sizin sorumluluğunuzdadır. + Halen \"Giriş verilerini kaydet\" işlevini kullanıyorsanız, gelecekte sağlık kartınıza ve PIN kodunu girmeden e-reçete uygulamasıyla reçeteleri görüntüleyebilecek, erişebilecek, kullanabilecek veya silebileceksiniz. + Konum bulunamadı + Eczaneye gönder + Kodu göster + Dil + Dil + Almanca + Arapça + Bulgarca + Çek + Danimarka + İngilizce + Fransızca + İbranice + İtalyan + Flemenkçe + Lehçe + Romen + Rusça + Türkçe + Ukrayna + varsayılan + Dijital maliyet fişleri devre dışı bırakıldı ve silindi. + “E-reçete forumu” + Yedek hazırlık mümkün değil + Muadil mümkün + İkame hazırlığı (Aut idem) + Eczacılar, hastanın sağlık sigortası şirketinin ilaç üreticileriyle indirim anlaşması yaptığı ilaçların dağıtımına öncelik vermekle yükümlüdür. Bu durum yalnızca doktorun reçetede \"Aut idem\" ifadesini hariç tutması durumunda geçerli değildir; sizin reçetenizde durum böyle değildir. + Doktorunuz reçete edilen preparatı almanız gerektiğine karar vermiştir. Eczane, indirim sözleşmesi (“Aut idem”) temelinde değişim yapmamalıdır. + Ekran görüntülerine izin ver + Ekran görüntülerine izin verirseniz uygulama değiştirdiğinizde açtığınız son sayfa arka planda görünür durumda kalır. Gerektiğinde kişisel verileriniz görünebilir. Bu nedenle ekran görüntülerine izin verilmemesini öneririz. + İzin ver + İptal et + Profil resminiz için klavyenizi çıkartmalara değiştirin veya emojileri kullanın. + Profil resmini seç + Nasıl devam etmek istiyorsunuz? + Fotoğraf seç + kamera + Emoji + İptal et + Ayarları aç + Kullanmak + bir fotoğraf çek + Profil resmini düzenle + %s dakika önce varsayıldı + %s tarihinde kabul edildi + Yeni kabul edildi + Saat %s de kabul edildi + Sağlık Kimliği + Sigortayı seç + Health ID\'ye kaydolma işlemi beklendiği gibi gitmezse lütfen yardım merkezimizdeki ipuçlarını takip edin. + Yardım + Yardım + Sigorta uygulamasına kaydolmaya yönelik ipuçları + Sigorta şirketiniz Sağlık Kimliğinizden sorumludur. Kayıtla ilgili herhangi bir sorunuz varsa lütfen onlarla iletişime geçin. İşte denenmiş ve test edilmiş bazı ipuçları: + Sigortanıza bağlı olarak ayrı bir uygulamanın gerekli olabileceğini lütfen unutmayın. Bunun ne olduğunu öğrenmek için lütfen sigorta şirketinize danışın. + Yazarkasa uygulamasını başlatın ve e-reçete uygulamasına kaydolmaya başlamadan önce bir kez orada oturum açın. + Sağlık Kimliği ile sağlık kartına kaydolma arasında geçiş yapmak sorunlara yol açabilir. Bu nedenle lütfen giriş seçeneğini değiştirmeden önce profilinizden aktif olarak çıkış yapın. + Sigortanız listede yoksa, alternatif olarak sağlık kartınız ve ilgili PIN kodunuzla kayıt olabilirsiniz. + Yazarkasa uygulaması sizi e-reçete uygulamasına geri yönlendirmiyorsa bu hatayı mutlaka sigorta şirketinize bildirin. + Sigorta uygulamasına erişilemiyorsa akıllı telefonunuzun tarayıcı ayarlarına giderek bağlantıların açılmasına izin vermeniz faydalı olabilir. + Akıllı telefonunuz Android 14 çalıştırıyorsa ayarlarda bağlantıların açılmasına izin vermeniz yararlı olabilir. + Ayarları aç + Bu maalesef olmadı + Lütfen daha sonra tekrar deneyiniz. + Yükleme sırasında hata mesajları. + Lütfen bir emoji veya metin seçin + Kaydet + devam etmek için lütfen giriş yapınız + İleri + Arama + Yazmak\nPosta + Eczaneye + Eczane + Bugün + Yarın + Burada\nrota + Reçeteyi ve maliyet makbuzunu sil + Tarifi silerseniz ilgili maliyet fişi de silinir. + Artık tüm profillerden gelen mesajlar birlikte görüntüleniyor + Yeni özellikler + Yeni özellikler + Sürekli olarak yeni özellikler üzerinde çalışıyoruz. Fikrinizi bizimle paylaşın ve daha iyi bir uygulama geliştirmemize yardımcı olun. + Herkes için siparişler + Siparişler altında artık tüm profillere ait siparişleri bir arada görebilir ve çok daha kolay görüntüleyebilirsiniz. + + Masrafların geri ödenmesi yok + Kural olarak sağlık sigortası bu reçete masraflarını karşılamamaktadır. Bu nedenle hasta olarak ödemenin tamamından siz sorumlusunuz. Masrafların ek sigorta veya yasal yardımlar kapsamında geri ödenip ödenmeyeceği ayrı ayrı kontrol edilmelidir. + Sigortanız bu reçeteye ilişkin herhangi bir masrafı karşılamayacaktır. + + "Sigortanız reçeteyi %s karşılamayacaktır " + "Sigortanız %s reçetelerini karşılamayacaktır." + + neden oldu + Tarih + Kaza + İş kazası + Endüstriyel hastalık + Haberler + Haber yok + Henüz herhangi bir mesajınız yok. + 🎉 Siparişiniz teslime hazır. Kendinizi tanıtmak için lütfen bu teslim alma kodunu gösterin. + Maalesef eczanenizden gelen mesaj boştu. Lütfen eczanenizle iletişime geçin. + Eczane size bir bağlantı sağladı. + Haberler + Emir + Mesajlar + İnternet bağlantısı yok + Bağlı cihazları görüntülemek için biyometriye bağlı olmanız gerekir. + Reçete %s tarihinde silindi + Silindi + Reçete yok + Arşivinizde tarifiniz yok. + Masraf makbuzlarınız silindi + Maliyet makbuzu + Kayıt gerekli + Bağlı cihazları görüntülemek için lütfen oturum açın. + Giriş yapmak + Giriş yapmak + %s Sigortaya bağlanın + Akıllı telefonda sağlık kartı + Oturum açılmamış + Kayıtlı + yenileme + Reçete hizmetlerine kaydolduysanız erişim protokolleri alırsınız. + Kayıt gerekli + Bağlı cihazları görüntülemek için lütfen sağlık kartınızla giriş yapın ve biyometri ile giriş bilgilerinizi kaydedin. + %1$s x Sabah + %1$s x Öğle Yemeği + %1$s x Akşam + %1$s x Gece + Doktorunuz size farklı bir talimat vermediği sürece kullanım talimatlarını şu şekilde anlayabilirsiniz: + Doktorunuz size ilacın kullanımıyla ilgili bu bilgiyi vermiştir. + Reçetenizde ilacı nasıl alacağınıza dair bir bilgi yok. + Doktorunuz, reçetede yer almayan bu ilacı nasıl kullanacağınız konusunda size talimat verildiğini kaydetti. Bu, örneğin ilaç planınızda olabilir. + Belirtilmedi + DJ + Kullanım talimatları + " %s - %s" + Gelir hatırlatıcısı + Hatırlatıcıları al + Ağızdan alınan ilaçlar + Giriş hatırlatmaları hakkında + Sabah + Öğle vakti + Öğleden sonra + Akşam + ilaç + Dozaj bilgisi + 1 + %s / %s + 1 doz + Dozu değiştir + İptal et + İlaç hatırlatıcısı + İlaç programı + Dozu doktor reçetesine göre alın + Saat %s \'de %s x alın + Tarihleri ​​seçin + İleri + İptal etmek + A + dışında + İzleme + Sınırsız + Bireysel olarak + İlk gün + Son gün + Zaman ekle + Giriş hatırlatıcısı yok + Reçeteleriniz için hatırlatıcı ayarlayabilirsiniz + Açık + Kapalı + Beni Hatırla + Bu uygulama için pil optimizasyonunu kapatın. + Bu seçeneği etkinleştirmezseniz ilaç hatırlatmaları güvenilmez görünebilir. + İptal et + İzin ver + Kullanım talimatları + Belirtilmedi + bilgi + Pil optimizasyon modunda da unutmayın + Dozu değiştir + Miktar + biçim + sona erdi + sınırsız + %s ye + Tekrarla %s + Mümkün olduğunda hesaplama için reçetede saklanan bilgileri kullanırız. + İptal et + Kaydet + Paketin sonuna kadar + Zaman + doz + İlaçları görüntüle + İlaçlarınızı zaten aldınız mı? + Hatırlatıcıları al + Aktif anı yok + Bugün alman gereken hatırlatıcın yok + Başlangıç ​​tarihini seçin + Bitiş tarihini seçin + Hoş geldin + e-reçete uygulamanızda + Tekrar deneyin + Şifreyi girin + Lütfen bu cihazı paylaşabileceğiniz ve giriş bilgilerinizi bilen herkesin tariflerinize erişebildiğinden emin olun. + Şifreyi girin + Uygulamanın kilidini açmak için lütfen şifreyi girin. + şifre + şifre + Uygulamanın güvenliğini sağlamak için lütfen bir şifre girin. + Şifreyi göster + Şifreyi tekrar girin + Şifrenizi mi unuttunuz? Lütfen uygulamayı silin ve ardından yeniden yükleyin. Nedenini %s dosyamızda öğrenebilirsiniz. + Şifreyi girin + Şifre en az sekiz karakter uzunluğunda olmalıdır + Şifre gücü yeterli değil + Şifre gücü yeterli + Cihaz yedekleme mümkün değil + Lütfen önce bir cihaz yedeklemesi ayarlayın. + İptal etmek + Ayarlar + Şifre gücü çok iyi + Uygulama güvenliği + Cihaz yedekleme + Lütfen uygulamayı yedeklemek için en az bir yöntem seçin. + şifre + Şifre değiştir + Silmek için Reçete Sunucusu ile bağlantı kurulmalıdır + Mallar hazır + Mallar %s tarihinden beri hazır. + Mallar şimdi hazır + Ürünler %s dakikadır hazır + Mallar %s tarihinden beri hazır. + Şifre girilmediğinden devre dışı bırakıldı. + Şifre çok zayıf olduğundan devre dışı bırakıldı. + Şifreler eşleşmediğinden devre dışı bırakıldı. + Keşfetmek + Organ bağışı kaydı + Organ bağışı kaydı açılsın mı? + Organ bağışçısı kayıt sayfasına yönlendirileceksiniz. Organ bağışı verilerinizi görüntülemek ve değiştirmek için buraya giriş yapmalısınız. + Açık + İptal etmek + İşlev demo modunda etkin değil diff --git a/app/features/src/main/res/values-uk/strings.xml b/app/features/src/main/res/values-uk/strings.xml index c4efe82d..9b153db8 100644 --- a/app/features/src/main/res/values-uk/strings.xml +++ b/app/features/src/main/res/values-uk/strings.xml @@ -28,7 +28,7 @@ Не скасовувати Ходімо Вам потрібно: - Введіть номер доступу до картки + Введіть номер доступу Ввести PIN-код Спробуйте ще раз Помилка з’єднання з сервером @@ -59,26 +59,23 @@ Вихідні дані Видавець gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Керуючий директор: Dr. Флоріан Хартге\n Реєстраційний суд: окружний суд Берліна-Шарлоттенбурга\n Номер у комерційному реєстрі: HRB 96351\n Ідентифікаційний номер ПДВ: DE241843684 + Керівництво: Dr. Флоріан Фурманн, Бренья Аджей, д-р. Florian Hartge\nРеєстраційний суд: Окружний суд Берліна-Шарлоттенбурга\nНомер комерційного реєстру: HRB 96351\nІдентифікаційний номер ПДВ: DE241843684 Відповідальний за контент - Доктор. Флоріан Хартге + доктор Флоріан Фурманн, Бренья Аджей, д-р. Флоріан Хартге Контакт Указівка Ми намагаємося використовувати гендерно нейтральну мову. Якщо ви помітили помилки, ми будемо раді вашим повідомленням електронною поштою. Сучасна німецька платформа для цифрової медицини Написати електронне повідомлення Відкрити вебсайт - Вітаємо! - Розпочати реєстрацію Розблокувати зареєструватися Скасувати - Безпека Правові питання Вихідні дані Захист даних Умови використання - Деталі + Деталі рецепта Позначити як погашено Позначити як не погашено Лікарська форма @@ -100,8 +97,6 @@ Номер виробничого майданчика Номер телефону Адреса ел. пошти - Нещасний випадок на виробництві - День нещасного випадку Номер місця нещасного випадку або номер роботодавця Видалити цей рецепт безповоротно? Видалити @@ -124,7 +119,6 @@ Відкрити сканер для рецептів Налаштування Придушити скріншоти - Запобігає відображенню заставки під час переходу з одного застосунку до іншого Чи дозволяєте ви E-Prescription анонімно аналізувати вашу поведінку при використанні? Технічна інформація Безпека ваших даних рецепта @@ -146,23 +140,15 @@ Анонімний аналіз залишається деактивованим %s Дякуємо за Вашу підтримку! зареєструватися - Щоб завантажити рецепти, ідентифікуйте себе. - Примітка для аптек: ми отримуємо контактні дані та інформацію про аптеки від mein-apothekenportal.de Німецької аптечної асоціації Deutsche Apothekenverband e.V. Ви виявили помилку чи хотіли б виправити дані? + Будь ласка, вкажіть себе. + Примітка для аптек: контактні дані та інформацію про аптеки ми отримуємо від mein-apothekenportal.de. Ви знайшли помилку чи бажаєте виправити дані? Детальніше - аптеках + Aптеках На жаль, спроба невдала \uD83D\uDE15 Будь ласка, спробуйте ще раз. - Введіть пароль - Далі Довідки з керування - Масштабувати - Дозволяє збільшувати масштабування застосунку зведенням або розведенням пальців (зведення для масштабування). - Пароль - Захистіть свої дані паролем на свій вибір. - Пароль + Увімкнути масштабування Зберегти - Відобразити пароль - Повторно введіть пароль Рекомендації: %s Написати електронне повідомлення Під час надсилання своїх повідомлень буде передаватися вказана нижче інформація про обладнання та операційну систему, що використовується: @@ -173,7 +159,7 @@ Розсилання Фільтр Фільтрувати - Жодна локація не доступна + Поділіться місцем розташування Зрозуміло Паролі не збігаються Помилка 20 10 76631 @@ -185,8 +171,6 @@ Виявлено %s невдалих спроб входу. - Вибрати найкращий захист пристрою - Це може бути відбиток пальця, графічний ключ або щось подібне. Токени Токен доступу Токени SSO @@ -194,7 +178,7 @@ не доступний жоден токен SSO скопійовано в буфер обміну Натисніть, щоб скопіювати токен у буфер обміну - Діє лише сьогодні + Викупити можна лише сьогодні Дозволити Підключення до сервера відсутнє Спробуйте ще раз через кілька хвилин. @@ -262,7 +246,7 @@ %s нових рецептів Можна погасити - У спокуті + Перебуває в обробці Погашено Невідомо Відобразити протоколи доступу @@ -274,7 +258,7 @@ Наразі рецепт обробляється, і його неможливо видалити. прийняти Мабуть, спроба невдала. - Ми знаємо, що підключення до медичної картки має свої підводні камені. У майбутньому реєстрація також повинна бути можливою через уже автентифікований додаток медичного страхування. \n\n Ми також працюємо над тим, щоб рецепти можна було викупити в цифровому вигляді без реєстрації. \n\n Чи помітили ви щось під час цього процесу, чим хотіли б поділитися з нами? Будь ласка, напишіть нам, ми також будемо раді отримати дуже критичні відгуки. + Ми знаємо, що підключення до медичної картки має свої підводні камені. У майбутньому реєстрація також повинна бути можливою через уже автентифікований додаток медичного страхування.\n\nМи також працюємо над тим, щоб рецепти можна було викупити в цифровому вигляді без реєстрації.\n\nЧи помітили ви щось під час цього процесу, чим хотіли б поділитися з нами? Будь ласка, напишіть нам, ми також будемо раді отримати дуже критичні відгуки. Поради щодо підключення Покращте силу з\'єднання За можливості видаліть захисну оболонку @@ -297,7 +281,7 @@ Дата сканування: %s Дата позначення як погашено: %s Бажаєте продовжити? - Замовити + Відправити в аптеку Незабаром буде в наявності Забронюйте зараз для отримання або доставте його кур\'єрською службою чи доставкою Зберегти для пізніших замовлень @@ -329,13 +313,11 @@ https://www.openstreetmap.org/copyright Конфіденційність і використання Далі - Ви отримали PIN-код для своєї медичної картки від вашої страхової компанії за допомогою безпечної процедури, такої як Postident. + Ви повинні були активно замовити PIN-код своєї медичної картки у своїй медичній страховій компанії, а потім отримати його через безпечний процес, такий як Postident. PIN-код не отримано PIN Перевірте підключення до Інтернету та налаштування часу/дати на вашому пристрої. - Для входу натисніть «Розблокувати». Заблоковано? Перевірте свої біометричні облікові дані на цьому пристрої. - Забули пароль? Видаліть застосунок, а потім повторно встановіть його. Чому це так, ви можете дізнатися в %s Довідка Розмір упаковки та одиниця Активна речовина @@ -348,10 +330,6 @@ Скасувати Указівка Допоможіть нам зробити цю програму кращою - Введіть пароль - Пароль повинен містити не менше восьми символів - Надійність пароля не достатня - Надійність пароля достатня Пароль видимий. Пароль не видимий. Біометрія @@ -458,9 +436,6 @@ Відправлено щойно Час відправлення: %s Більше не дійсний - Увійдіть за допомогою застосунку - Вибрати страховку - Не знайшли те, що шукали? Цей список постійно розширюється. Реєстрація за медичною карткою вже підтримується кожною медичною страховою компанією. Відгук із застосунку E-Rezept Ми з нетерпінням чекаємо на ваші відгуки. Використовуйте місце нижче та формулюйте свої думки якомога точніше: PUK @@ -474,13 +449,13 @@ Указівка З міркувань безпеки підключення до сервера рецептів припиняється через 12 годин. Для повторного підключення вам знадобиться медична картка та PIN-код для кожного процесу підключення. На цьому пристрої не налаштовано біометричний захист. - Неможливо зберегти дані доступу. Заздалегідь налаштуйте біометричний захист (наприклад, відбиток пальця) на своєму пристрої. + Неможливо зберегти дані доступу. Попередньо налаштуйте біометричний захист (наприклад, відбиток пальця) на своєму пристрої. Скасувати Налаштування Указівка прийняти Безпека ваших даних рецепта - \"Цей застосунок використовує найбезпечніший біометричний датчик, наданий вашим пристроєм, щоб зберігати ваші облікові дані в безпечній області пам’яті пристрою.\" + \Ця програма використовує найбезпечніший біометричний датчик, який надає ваш пристрій, щоб захистити ваші облікові дані в захищеній зоні пам’яті пристрою.\ Біометрична безпека ваших даних доступу дозволяє вам відкривати цю програму в майбутньому, не вводячи свій PIN-код або картку здоров’я, а також переглядати, викликати, використовувати або видаляти рецепти. Майте на увазі, що особи, з якими ви, можливо, спільно користуєтеся цим пристроєм і чиї біометричні функції можуть зберігатися на цьому пристрої, також матимуть доступ до ваших рецептів. На жаль, спроба невдала. @@ -493,7 +468,7 @@ Прізвище Страхування Страховий номер - Номер доступу до картки + Номер доступу зареєструватися Вийти з системи Зберегти @@ -509,8 +484,7 @@ зв\'язок втрачений Підключитися до сервера рецептів? Ніяких жетонів - Ви отримаєте жетон під час входу в службу рецептів.\n - замовлення + Ви отримаєте токен, якщо ви увійшли в службу рецептів. Виберіть потрібний PIN-код розблокувати картку Виберіть PIN-код @@ -527,26 +501,26 @@ Отримати код для самовивозу Неможливо відобразити повідомлення Будь ласка, зверніться до вашої аптеки ( %s ). - Показати посилання на кошик для покупок + Посилання на аптеку Показати код для самовивозу Показати повідомлення %s о годині %s - Рецепт надіслано %s . Процес цифрового викупу досі незнайомий для багатьох аптек. Якщо ви не отримаєте відповідь до завтра, рекомендуємо зателефонувати, щоб дізнатися, як запобіжний захід. + Рецепт надіслано %s . Деякі аптеки ще не мають цифрової відповіді. Якщо до завтра не буде відповіді, зателефонуйте як запобіжний захід. Огляд замовлення новий курс Замовити - Безкоштовно для абонента. Графік роботи: Пн – Пт 8:00 – 20:00, крім національних свят + Безкоштовно для абонента. Час роботи: Пн - Пт 8:00 - 20:00, крім державних свят Аптека Виберіть потрібний PIN-код Бажаний PIN-код збережено Наразі відкрито і біля мене Фільтрувати за… - почати пошук + шукати Пряме доручення аптеках Номер телефону (необов\'язково) - Пошук за назвою або адресою + шукати Немає дійсної інформації про аптеку Актуальної інформації про цю аптеку не знайдено. Запис про цю аптеку буде видалено. Ok @@ -566,16 +540,15 @@ Покращення продукту Анонімний аналіз Допоможіть нам покращити цей застосунок. Усі дані про використання збираються анонімно і використовуються виключно для покращення користувацького досвіду. - Безпека програми персональні налаштування Довідки з керування Покращення продукту Доданий рецепт Рецепт вже є Під час імпортування сталася помилка + Номер доступу Видалити Відсканований рецепт - Можливий замінник Забутий PIN-код %s Рецепт @@ -593,7 +566,7 @@ В налаштуваннях системи ви можете змінити це рішення у будь-який час. Продовжити прийняти - Ця програма використовує найбезпечніший метод, доступний вашим пристроєм. + Програма використовує найбезпечніший метод, який ви налаштували на своєму пристрої. Зберегти Виберіть Медикамент @@ -621,15 +594,13 @@ Що таке пряме доручення? При прямому направленні рецепт від практики або лікарні виписується безпосередньо в аптеці. Застраховані особи не повинні вживати жодних дій і не можуть втручатися в процес викупу. \n\n Прямі напрямки перераховані в додатку для електронних рецептів, щоб зробити ваше лікування більш прозорим для вас. Плата за аварійну службу - Іноді потрібно поспішати. Деякі рецепти можна отримати без доплати за екстрену допомогу, наприклад, вночі або у святкові дні. + Якщо рецепт видається між 20:00 або в неділю та святкові дні, може стягуватися додаткова плата в розмірі 2,50 євро. Ліки підлягають співоплаті Звільнено від додаткової оплати - Ті, хто має державне медичне страхування, повинні сплачувати додаткову плату в розмірі до десяти євро за рецептурні ліки. \n\n Розмір доплати залежить від вартості Вашого препарату. За ліки, які коштують менше 5 євро, ви повинні платити самі.\n За ліки, які коштують дорожче, потрібно заплатити десять відсотків від ціни, але не менше 5 євро, а максимум 10 євро. \n\n Діти та молодь віком до 18 років, як правило, звільняються від додаткової плати. \n\n Якщо ваші річні витрати на ліки перевищують ваш ліміт фінансового тягаря, ви можете бути звільнені від співоплати. Поговоріть про це зі своєю медичною страховою компанією. + Люди, які мають державне медичне страхування, зазвичай платять максимум 10 євро за рецептурні ліки. Вищі комісії можуть стягуватися, якщо запитується препарат певного виробника, на який не поширюється угода про знижку на медичне страхування («запит на препарат»). \n\n Діти та молодь до 18 років звільняються від додаткових платежів. \n\n Якщо рецепти викуплені пізніше ніж через 28 днів після їх виписки, витрати повинні бути оплачені в повному обсязі. \n\n Якщо ви витрачаєте багато ліків протягом року, ви можете подати заявку на звільнення від співоплати в своїй медичній страховій компанії Ви звільнені від сплати спільної оплати за цей препарат. Ваша медична страхова компанія покриває вартість ліків. Скільки дійсний цей рецепт? Протягом цього періоду ви можете отримати рецепт у будь-якій аптеці з максимальною додатковою оплатою 10 євро. - Можливий замінник - Відповідно до юридичних вимог вашої страхової компанії, вам можуть надати альтернативу з тим самим активним інгредієнтом. \n\n Ліки можуть виглядати і називатися по-різному, мати різні ціни та виробників, але містити однакову діючу речовину. Сам активний інгредієнт і дозування мають вирішальне значення для впливу ліків на організм. Пацієнти часто отримують в аптеці ліки, відмінні від тих, які призначив лікар, за умови, що ліки порівнюються. Для зміни можуть бути терапевтичні та економічні причини. Відсканований рецепт Рецепти, імпортовані з паперової копії, не можуть відображати особисту або медичну інформацію з міркувань безпеки. \n\n Увійдіть у цю програму за допомогою медичної картки або програми страхування, щоб переглянути всю інформацію, що міститься в рецепті. Рецепт невірний @@ -639,7 +610,7 @@ Телефон веб-сайт Ел. пошта - Сортування за відстанню неможливе. + Поділіться своїм місцезнаходженням, щоб знайти аптеки поблизу. Ok Введіть поточний PIN-код Введено неправильний PIN-код @@ -657,12 +628,10 @@ страхова картка Біометрія Не в системі - Нам цікава ваша думка. Будь ласка, приділіть п’ять хвилин, щоб заповнити наше опитування. Заздалегідь дуже дякую. Попередження Аптека додана в обране Аптека видалена з вибраного Мої аптеки - Надійність пароля дуже добра Операція запису не вдалася Не вдалося зберегти PIN-код Повідомити про порушення @@ -681,7 +650,6 @@ Використано %s Викупив щойно Використано о %s годині - замовлення Цей рецепт був викуплений для вас як частина лікування. Плата за аварійну службу Цей рецепт не можна отримати в аптеці вночі без додаткової оплати екстреної послуги. @@ -695,7 +663,7 @@ Отримувати рецепти в цифровому вигляді? Потягніть екран вниз, щоб оновити. Кожних рецептів - Увійдіть, щоб автоматично отримувати рецепти, або додайте новий рецепт, використовуючи ⊕ у верхньому куті. + Підпишіться, щоб автоматично отримувати рецепти. зареєструватися Архів рецептів Можливо пізніше @@ -732,9 +700,9 @@ Клацніть на дисплеї, щоб пропустити підказку, яка з’явиться. Як викупити? Як би ви хотіли отримати ваші ліки? - Викупити безпосередньо - Викупити ліки на місці - Замовити + Показати код + Відскануйте в аптеці + Відправити в аптеку Забронюйте або доставте Готово Код колекції @@ -802,7 +770,7 @@ Це видалить усі квитанції про витрати з цього пристрою та сервера. Отримувати квитанції про витрати Ваші чеки також зберігаються на сервері рецептів. - Отримано + Активувати Усього: %s %s Вибрати Спліт @@ -851,17 +819,17 @@ Потрібен пов\'язаний PIN-код Можна викупити лише завтра як самооплату Залишилося лише %s днів, щоб скористатися послугою самостійного сплати - \nВсе ще можна отримати як самооплату протягом %s днів\n - Дійсний лише протягом %s днів - \nЗалишилося %s днів\n - Діє лише завтра + Все ще можна отримати як самооплату протягом %s днів + Залишилося лише %s днів, щоб активувати + Ще можна отримати протягом %s днів + Викупити можна лише завтра Стягується плата Бере страховку Рецепт(и) успішно передано. Рецепт не підлягає обробці. Будь ласка спробуйте ще раз. Можливо, вам доведеться вибрати іншу аптеку. Рецепт не підлягає обробці. Аптека повідомляє про невідому помилку. Якщо потрібно, спробуйте іншу аптеку. Рецепт був відхилений аптекою. Рецепт може бути недійсним або ваша адреса доставки чи контактна інформація можуть бути недійсними. - Не вдалося активувати, перевірте підключення до Інтернету. + Спробуйте ще раз і, можливо, виберіть іншу аптеку. Якщо помилка не зникне, зверніться до служби підтримки. Рецепт успішно передано. Однак аптека повідомляє про помилку обробки. Будь ласка, зверніться в аптеку. Рецепт був відхилений аптекою. Рецепт вже погашений. Рецепт був відхилений аптекою. Рецепт видалено. @@ -894,7 +862,6 @@ Активуйте демонстраційний режим Не зараз Пошук - Потрібен смартфон із підтримкою NFC Отримуйте квитанції про витрати в цифровому вигляді Після активації квитанцій про вартість ви знайдете їх тут після погашення вашого рецепта. Активувати @@ -902,11 +869,11 @@ Щойно аптека депонує квитанцію, вона з’явиться тут. Витратна квитанція Отримуйте квитанції про витрати в цифровому вигляді - Примітка. Ви більше не отримуватимете квитанції про витрати у вигляді роздруківки в\n АПТЕКА.\n + Примітка. Ви більше не отримуватимете роздруковані квитанції в аптеці. Активувати Можливо пізніше Угода - З вашої згоди ви утримаєтеся від друку в аптеці\n і надсилайте свої квитанції про витрати в цифровому вигляді в майбутньому.\n + З вашої згоди ви утримаєтеся від їх роздрукування в аптеці та надалі надсилатимете чеки в цифровому вигляді. Скасувати Погоджуюся Вийти з демонстраційного режиму @@ -921,17 +888,17 @@ Немає Інтернету Немає підключення до Інтернету. Некоректний запит - Виникла проблема із запитом. Ми працюємо над рішенням.\n + Виникла проблема із запитом. Ми працюємо над рішенням. Сервер не відповідає Спробуйте ще раз через кілька хвилин. Не вдалося підключитися - Наразі неможливо отримати інформацію. Будь ласка\n спробуйте ще раз пізніше.\n + Наразі неможливо отримати інформацію. Будь-ласка спробуйте пізніше. Відхилено - Сервер відхилив ваш запит. Будь ласка, спробуйте\n знову пізніше.\n + Сервер відхилив ваш запит. Будь-ласка спробуйте пізніше. Оновити додаток - Щоб скористатися цією функцією, оновіть програму.\n + Щоб скористатися цією функцією, оновіть програму. Помилка входу в систему - На сервері виникла проблема під час входу. Будь ласка, зв\'яжіться\n знову.\n + На сервері виникла проблема під час входу. Увійдіть знову. зареєструватися Копіювати URL Підключіться до програми зовнішнього медичного страхування. @@ -957,12 +924,287 @@ Рекомендовано Цифровий ідентифікатор здоров\'я Налаштувати параметри? - 404 сторінка? Щоб продовжити, увімкніть відкривання посилань у налаштуваннях. Відкрийте налаштування Застаріла версія програми Ваша версія програми застаріла та більше не підтримується. Щоб і надалі користуватися електронним рецептом, оновіть додаток. Оновити - Ваш CAN містить %s цифр. + Ваш номер доступу складається з %s цифр. Ваш PIN-код може містити від %s до %s цифр. + Підніміть + Кур\'єр + Відвантаження + Потрібен смартфон із підтримкою NFC + Нам не вдалося обробити запит на видалення рецепту. Ми працюємо над рішенням ( %s ). + Щось пішло не так під час видалення мого рецепта, я отримав код помилки %s . Будь ласка, повідомте мене, коли видалення знову стане можливим. + Далі + Некоректний запит + Повідомити про порушення + Видалити локально + Немає Інтернету + Не вдалося видалити рецепт. Перевірте підключення до Інтернету та повторіть спробу. + Спробуйте ще раз + Ok + Повторний вхід + Ви повинні увійти, щоб видалити рецепт (401). + зареєструватися + Скасувати + Видалення неможливо + Наразі вам не дозволено видаляти рецепт, оскільки він знаходиться в аптеці, щоб його виписали, або це незавершене пряме призначення (403). + Забагато запитів + Ви забагато разів намагалися видалити рецепт. Спробуйте пізніше (429). + Видалити рецепт + Ой... + «Сталася неочікувана помилка сервера. Спробуйте пізніше (500).» + Безпека програми неможлива. Попередньо налаштуйте біометричний захист (наприклад, відбиток пальця) на своєму пристрої. + Біометричне резервне копіювання неможливе для цього пристрою, оскільки воно не підтримується вашим пристроєм. + Небезпека! Ваш телефон погано захищений. + Ваші дані для входу зберігаються на вашому телефоні. Щоб захистити доступ, використовується бажаний спосіб входу. Цей метод входу, наприклад, графічний шаблон, не дуже безпечний. Ви використовуєте функцію «Зберегти дані для входу» на власний ризик. + Якщо ви все ще використовуєте функцію «Зберегти дані для входу», у майбутньому ви зможете переглядати, отримувати доступ, викуповувати або видаляти рецепти за допомогою програми електронних рецептів без медичної картки та введення PIN-коду. + Розташування не знайдено + Відправити в аптеку + Показати код + Мова + Мова + Німецький + Aрабська + Болгарська + Чеська + Датська + Англійська + Французька + Іврит + Італійська + Голландська + Польський + Румунська + Російський + Турецька + Українська + Стандартний + Цифрові квитанції було дезактивовано та видалено. + «Форум електронних рецептів» + Замінна підготовка неможлива + Можливий замінник + Підготовка до заміни (Aut idem) + Фармацевти зобов’язані в першочерговому порядку відпускати ліки, на які страхова компанія пацієнта уклала з виробниками ліків угоду про знижки. Це не стосується лише випадків, коли лікар виключає «Aut idem» у рецепті, що не стосується вашого рецепту. + Ваш лікар вирішив, що ви повинні отримати призначений препарат. Аптека не повинна здійснювати обмін на підставі договору знижки («Aut idem»). + Дозволити знімки екрана + Якщо дозволити знімки екрана, остання відкрита сторінка залишатиметься видимою у фоновому режимі під час перемикання програм. При необхідності ваші особисті дані можуть бути видимі. Тому ми рекомендуємо не дозволяти знімки екрана. + Дозволити + Скасувати + Перемкніть клавіатуру на наклейки або використовуйте емодзі для зображення профілю. + Виберіть зображення профілю + Бажаєте продовжити? + Виберіть фото + камера + Емодзі + Скасувати + Відкрийте налаштування + використання + Сфотографуйте + Редагувати зображення профілю + Припущено %s хвилин тому + Прийнято %s + Щойно прийнято + Прийнято о %s годині + HealthID + Вибрати страховку + Якщо реєстрація за допомогою Health ID пройшла не так, як очікувалося, дотримуйтесь порад нашого довідкового центру. + Довідка + Довідка + Поради щодо реєстрації в додатку страхування + Ваша страхова компанія несе відповідальність за Health ID. Будь ласка, зв\'яжіться з ними, якщо у вас виникнуть запитання щодо реєстрації. Ось кілька перевірених порад: + Зауважте, що залежно від вашої страховки може знадобитися окремий додаток. Зверніться до своєї страхової компанії, щоб дізнатися, що це таке. + Запустіть касовий додаток і увійдіть у нього один раз, перш ніж розпочати реєстрацію в додатку електронних рецептів. + Переключення між реєстрацією за допомогою Health ID і медичною карткою може призвести до проблем. Тому, будь ласка, активно вийдіть зі свого профілю, перш ніж змінювати параметри входу. + Якщо вашої страховки немає в списку, ви можете зареєструватися за допомогою своєї медичної картки та відповідного PIN-коду. + Якщо програма касового апарату не перенаправляє вас назад до програми електронних рецептів, обов’язково повідомте про цю помилку свою страхову компанію. + Якщо немає доступу до програми страхування, можливо, буде корисно перейти в налаштування браузера смартфона та дозволити відкривати посилання. + Якщо ваш смартфон працює під керуванням Android 14, можливо, буде корисно дозволити відкривати посилання в налаштуваннях. + Відкрийте налаштування + На жаль, спроба невдала. + Повторіть спробу пізніше. + Повідомлення про помилки під час завантаження. + Будь ласка, виберіть емодзі або текст + Зберегти + Будь ласка, увійдіть, щоб продовжити + Далі + Телефонуйте + Напишіть\nПошта + В аптеку + Аптека + Сьогодні + Завтра + Маршрут\nсюди + Видалити рецепт і квитанцію + Якщо ви видалите рецепт, пов’язану квитанцію про витрати також буде видалено. + Повідомлення з усіх профілів тепер відображаються разом + Нові можливості + Нові можливості + Ми постійно працюємо над новими функціями. Поділіться з нами своєю думкою та допоможіть нам створити кращий додаток. + Замовлення для всіх + У розділі «Замовлення» тепер ви можете бачити замовлення для всіх профілів разом і переглядати їх набагато простіше + + Без відшкодування витрат + Як правило, медичне страхування не покриває витрати на цей рецепт. Таким чином, як пацієнт, ви несете відповідальність за повну оплату. Чи будуть витрати відшкодовуватися в рамках додаткового страхування чи передбачених законом виплат, необхідно перевіряти окремо. + Ваша страховка не покриває жодних витрат на цей рецепт. + + "Ваша страховка не покриє рецепт %s " + + + "Ваша страховка не покриє рецепти %s ." + + Спричинений + Дата + ДТП + Нещасний випадок на виробництві + Виробнича хвороба + Новини + Жодних новин + У вас ще немає повідомлень. + 🎉 Ваше замовлення готове до отримання. Будь ласка, покажіть цей код отримання, щоб ідентифікувати себе. + На жаль, повідомлення з вашої аптеки було порожнім. Зверніться до своєї аптеки. + Аптека надала вам посилання. + Новини + порядок + Повідомлення + Немає підключення до Інтернету + Для перегляду підключених пристроїв необхідно підключитися до біометрії. + Рецепт видалено %s + Видалено + Кожних рецептів + У вас в архіві немає рецептів. + Ваші квитанції видалено + Витратна квитанція + Потрібна реєстрація + Увійдіть, щоб переглянути підключені пристрої. + зареєструватися + зареєструватися + Підключіться до %s Insurance + Медична картка на смартфон + Не в системі + Зареєстрований + оновити + Ви отримаєте протоколи доступу, якщо ви увійшли в службу рецептів. + Потрібна реєстрація + Будь ласка, увійдіть за допомогою своєї картки здоров’я та збережіть дані для входу з біометричними даними, щоб переглянути підключені пристрої. + %1$s x Ранок + %1$s x Обід + %1$s x вечір + %1$s x Вночі + Якщо ваш лікар не дав вам інших вказівок, інструкції із застосування можна розуміти так: + Ваш лікар дав вам цю інформацію щодо прийому ліків. + У вашому рецепті немає інформації про те, як приймати ліки. + Ваш лікар зазначив, що ви отримали інструкції щодо прийому цього препарату, який не відпускається за рецептом. Це може бути, наприклад, у вашому плані лікування. + Дані відсутні + діджей + Інструкція із застосування + " %s - %s" + Нагадування про доходи + Приймайте нагадування + Пероральні препарати + Про нагадування про прийом + Ранок + полудень + полудень + Ввечері + Медикамент + Інформація про дозування + 1 + %s / %s + 1 доза + Змінити дозування + Скасувати + Нагадування про ліки + Графік прийому ліків + Приймайте дозування згідно з призначенням лікаря + О %s годині візьміть %s х + Виберіть дати + Далі + Скасувати + А + з + Відстеження + Необмежений + Індивідуально + Перший день + Останній день + Додайте час + Немає нагадувань про прийом + Ви можете встановити нагадування для своїх рецептів + Увімкнути + Вимкнено + пам\'ятай мене + Вимкніть оптимізацію акумулятора для цієї програми. + Якщо ви не активуєте цю опцію, нагадування про прийом ліків можуть здаватися ненадійними. + Скасувати + Дозволити + Інструкція із застосування + Дані відсутні + інформації + Також пам’ятайте про режим оптимізації батареї + Змінити дозування + Кількість + форму + закінчився + необмежений + до %s + Повторити %s + За можливості ми використовуємо інформацію, що зберігається в рецепті, для розрахунку. + Скасувати + Зберегти + До кінця пачки + час + доза + Переглянути ліки + Ви вже прийняли ліки? + Приймайте нагадування + Немає активних спогадів + Сьогодні у вас немає нагадувань + Виберіть дату початку + Виберіть кінцеву дату + Ласкаво просимо + у вашому додатку для електронних рецептів + Спробуйте знову + Введіть пароль + Будь ласка, переконайтеся, що кожен, з ким ви можете поділитися цим пристроєм і хто знає вашу реєстраційну інформацію, також має доступ до ваших рецептів. + Введіть пароль + Введіть пароль, щоб розблокувати програму. + пароль + пароль + Введіть пароль, щоб захистити програму. + Показати пароль + Повторіть пароль + Забули пароль? Видаліть програму, а потім перевстановіть її. Ви можете дізнатися чому в нашому %s. + Введіть пароль + Пароль має бути не менше восьми символів + Надійність пароля недостатня + Достатня надійність пароля + Резервне копіювання пристрою неможливо + Спочатку налаштуйте резервне копіювання пристрою. + Скасувати + Налаштування + Надійність пароля дуже добра + Безпека програми + Резервне копіювання пристрою + Виберіть принаймні один метод резервного копіювання програми. + пароль + Змінити пароль + Для видалення необхідно встановити з’єднання з сервером рецептів + Товар готовий + Товари були готові з %s + Товар тільки зараз готовий + Товари готові протягом %s хвилин + Товари були готові з %s + Вимкнено, оскільки не введено пароль. + Вимкнено, оскільки пароль занадто слабкий. + Вимкнено, оскільки паролі не збігаються. + Досліджуйте + Реєстр донорства органів + Відкрити реєстр донорства органів? + Ви будете перенаправлені до реєстру донорів органів. Щоб переглядати та змінювати дані про пожертвування органів, ви повинні увійти в систему. + ВІДЧИНЕНО + Скасувати + Функція не активна в демонстраційному режимі diff --git a/app/features/src/main/res/values/strings.xml b/app/features/src/main/res/values/strings.xml index b3d979ba..3d53253a 100644 --- a/app/features/src/main/res/values/strings.xml +++ b/app/features/src/main/res/values/strings.xml @@ -27,7 +27,7 @@ Nicht abbrechen Los geht\'s Was Sie benötigen: - Kartenzugangsnummer eingeben + Zugangsnummer eingeben PIN eingeben Erneut probieren Verbindung mit dem Server herstellen fehlgeschlagen. @@ -60,9 +60,9 @@ Impressum Herausgeber gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Geschäftsführer: Dr. Florian Hartge\nRegistergericht: Amtsgericht Berlin-Charlottenburg\nHandelsregister-Nr.: HRB 96351\nUmsatzsteueridentifikationsnummer: DE241843684 + Geschäftsführung: Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge\nRegistergericht: Amtsgericht Berlin-Charlottenburg\nHandelsregister-Nr.: HRB 96351\nUmsatzsteueridentifikationsnummer: DE241843684 Verantwortlich für den Inhalt - Dr. Florian Hartge + Dr. Florian Fuhrmann, Brenya Adjei, Dr. Florian Hartge Kontakt https://www.das-e-rezept-fuer-deutschland.de/kontakt app-feedback@gematik.de @@ -71,17 +71,14 @@ Deutschlands moderne Plattform für digitale Medizin Mail schreiben Webseite öffnen - Willkommen - Anmeldung starten Entsperren Anmelden Abbrechen - Sicherheit Rechtliches Impressum Datenschutz Nutzungsbedingungen - Details + Rezeptdetails Als eingelöst markieren Als nicht eingelöst markieren Darreichungsform @@ -103,8 +100,6 @@ Betriebsstätten-Nummer Telefonnummer Mailadresse - Arbeitsunfall - Unfalltag Unfallbetrieb- oder Arbeitgebernummer Möchten Sie dieses Rezept unwiderruflich löschen? Löschen @@ -127,7 +122,6 @@ Scanner für Rezepte öffnen Einstellungen Screenshots unterdrücken - Verhindert die Anzeige eines Vorschaubilds beim App-Wechsel Erlauben Sie E-Rezept Ihr Nutzungsverhalten anonym zu analysieren? Technische Informationen Sicherheit Ihrer Rezeptdaten @@ -151,8 +145,8 @@ %s Vielen Dank für Ihre Unterstützung! \u2661 Anmelden - Bitte identifizieren Sie sich um Rezepte herunterzuladen. - Hinweis für die Apotheken: Die Kontaktdaten und Informationen zu Apotheken beziehen wir von mein-apothekenportal.de des Deutschen Apothekenverbandes e.V. Sie haben einen Fehler entdeckt oder möchten Daten korrigieren? + Bitte identifizieren Sie sich. + Hinweis für die Apotheken: Die Kontaktdaten und Informationen zu Apotheken beziehen wir von mein-apothekenportal.de. Sie haben einen Fehler entdeckt oder möchten Daten korrigieren? mein-apothekenportal.de Mehr erfahren https://www.mein-apothekenportal.de/ @@ -160,17 +154,9 @@ Apotheken Das hat leider nicht geklappt \uD83D\uDE15 Bitte probieren Sie es erneut. - Kennwort eingeben - Weiter Bedienungshilfen - Zoomen - Ermöglicht das Vergrößern der App über das Zusammen- oder Auseinanderziehen der Finger (Pinch-to-Zoom). - Kennwort - Sichern Sie Ihre Daten mit einem selbstgewählten Passwort. - Kennwort + Zoomen ermöglichen Speichern - Kennwort anzeigen lassen - Kennwort wiederholen Empfehlungen: %s Mail schreiben Beim Senden Ihrer Nachricht werden folgende Informationen über genutzte Hardware und Betriebssystem übertragen: @@ -181,7 +167,7 @@ Versand Filter Filtern - Kein Standort verfügbar + Standort freigeben Verstanden Wiederholtes Passwort stimmt überein Fehler 20 10 76631 @@ -191,8 +177,6 @@ Es wurde %s erfolgloser Anmeldeversuche festgestellt. Es wurden %s erfolglose Anmeldeversuche festgestellt. - Beste Gerätesicherung wählen - Hierbei kann es sich um Fingerabdruck, Wischmuster oder ähnliches handeln Tokens Access Token SSO Token @@ -200,7 +184,7 @@ kein SSO Token verfügbar in die Zwischenablage kopiert Klicken, um den Token in die Zwischenablage zu koperen - Nur noch heute gültig + Nur noch heute einlösbar Erlauben Keine Verbindung zum Server Bitte probieren Sie es in einigen Minuten erneut @@ -272,7 +256,7 @@ %s neue Rezepte Einlösbar - In Einlösung + Wird bearbeitet Eingelöst Unbekannt Zugriffsprotokolle anzeigen @@ -311,7 +295,7 @@ Gescannt am %s Als eingelöst markiert am %s Wie möchten Sie fortfahren? - Bestellen + An Apotheke senden Demnächst verfügbar Jetzt zur Abholung reservieren oder per Botendienst oder Versand liefern lassen Speichern für spätere Bestellung @@ -341,13 +325,11 @@ https://www.openstreetmap.org/copyright Datenschutz & Nutzung Weiter - Die PIN für Ihre Gesundheitskarte haben Sie durch ein sicheres Verfahren, wie dem Postident, von Ihrer Krankenversicherung erhalten. + Ihre PIN zur Gesundheitskarte mussten Sie aktiv bei Ihrer Krankenversicherung bestellen und erhielten sie dann über ein sicheres Verfahren wie Postident. Keine PIN erhalten PIN Überprüfen Sie die Verbindung mit dem Internet und die Uhrzeit-/Datumseinstellung Ihres Geräts. - Um sich anzumelden, drücken Sie auf “Entsperren”. Ausgesperrt? Bitte überprüfen Sie Ihre biometrischen Zugangsdaten auf diesem Gerät. - Passwort vergessen? Bitte löschen Sie die App und installieren sie anschließend erneut. Weshalb das so ist, erfahren Sie in unserem %s. https://www.das-e-rezept-fuer-deutschland.de/faq Hilfebereich Packungsgröße und Einheit @@ -361,10 +343,6 @@ Rückgängig Hinweis Helfen Sie uns, diese App besser zu machen - Kennwort eingeben - Das Kennwort muss mindestens acht Zeichen lang sein - Kennwortstärke nicht ausreichend - Kennwortstärke ausreichend Passwort ist sichtbar Passwort ist nicht sichtbar Biometrie @@ -424,7 +402,7 @@ Neue PIN wählen Ihre neue PIN können Sie selbst wählen (6 bis 8 Stellen). PIN gemerkt? - Bitte notieren Sie sich Ihre PIN und bewahren an einem sicheren Ort auf. + Bitte notieren Sie sich Ihre PIN und bewahren diese an einem sicheren Ort auf. Abbrechen Okay Entsperrung nicht möglich @@ -469,9 +447,6 @@ Gesendet gerade eben Gesendet um %s Uhr Nicht mehr gültig - Mit App anmelden - Versicherung wählen - Nicht fündig geworden? Diese Liste wird ständig erweitert. Die Anmeldung mit Gesundheitskarte wird bereits jetzt von jeder Krankenversicherung unterstützt. Feedback aus der E-Rezept App Wir freuen uns auf Ihr Feedback. Bitte nutzen Sie den folgenden Platz und formulieren Sie so präzise wie möglich: PUK @@ -486,13 +461,13 @@ Hinweis Aus Sicherheitsgründen wird die Verbindung zum Rezeptserver nach 12 Stunden getrennt. Für eine erneute Verbindung benötigen Sie für jeden Verbindungsvorgang Gesundheitskarte und PIN. Biometrische Absicherung einrichten - Zugangsdaten speichern nicht möglich. Richten Sie zuvor auf Ihrem Gerät eine biometrische Absicherung (z.B. Fingerabruck ) ein. + Zugangsdaten speichern nicht möglich. Richten Sie zuvor auf Ihrem Gerät eine biometrische Absicherung (z.B. Fingerabruck) ein. Abbrechen Einstellungen Hinweis Akzeptieren Sicherheit Ihrer Rezeptdaten - \"Diese App verwendet den sichersten biometrischen Sensor, der von Ihrem Gerät zur Verfügung gestellt wird, um Ihre Zugangsdaten in einem geschützten Bereich des Gerätespeichers zu sichern. \" + \Diese App verwendet den sichersten biometrischen Sensor, der von Ihrem Gerät zur Verfügung gestellt wird, um Ihre Zugangsdaten in einem geschützten Bereich des Gerätespeichers zu sichern.\ Die biometrische Sicherung Ihrer Zugangsdaten erlaubt es, diese App in Zukunft ohne Gesundheitskarte und Eingabe der PIN zu öffnen, Rezepte einzusehen, abzurufen, einzulösen oder zu löschen. Bitte achten Sie darauf, dass Personen, mit denen Sie gegebenenfalls dieses Gerät teilen und deren biometrische Merkmale auf diesem Gerät gespeichert sein könnten, ebenfalls Zugriff auf Ihre Rezepte erhalten. Das hat leider nicht geklappt @@ -505,7 +480,7 @@ Name Versicherung Versichertennummer - Kartenzugangsnummer + Zugangsnummer Anmelden Abmelden Speichern @@ -521,8 +496,7 @@ Verbindung getrennt Jetzt mit dem Rezeptserver verbinden? Keine Tokens - Sie erhalten einen Token, wenn Sie am Rezeptdienst angemeldet sind.\n - Bestellungen + Sie erhalten einen Token, wenn Sie am Rezeptdienst angemeldet sind. https://gematik.shortcm.li/E-Rezept-App_Feedback Wunsch-PIN wählen Karte entsperren @@ -540,26 +514,26 @@ Abholcode erhalten Nachricht kann nicht angezeigt werden Bitte kontaktieren Sie Ihre Apotheke (%s). - Warenkorb-Link anzeigen + Link der Apotheke Abholcode anzeigen Nachricht anzeigen %s um %s Uhr - Rezept an %s gesendet. Der digitale Einlöseprozess ist für viele Apotheken noch ungewohnt. Sollten Sie bis morgen keine Rückmeldung erhalten, empfehlen wir, vorsichtshalber telefonisch nachzufragen. + Rezept an %s gesendet. Einige Apotheken haben noch keine digitale Antwortmöglichkeit. Falls bis morgen keine Rückmeldung erfolgt, rufen Sie bitte vorsichtshalber an. Bestellübersicht Neu Verlauf Die Bestellung - Kostenfrei für den Anrufenden. Servicezeiten: Mo - Fr 08:00 - 20:00 Uhr außer an bundeseinheitlichen Feiertagen + Kostenfrei für den Anrufenden. Servicezeiten: Mo – Fr 08:00 – 20:00 Uhr außer an bundeseinheitlichen Feiertagen Apotheke Wunsch-PIN wählen Wunsch-PIN gespeichert Aktuell geöffnet und in meiner Nähe Filtern nach … - Suche starten + Suchen Direktzuweisung Apotheken Telefonnummer (optional) - Nach Name oder Adresse suchen + Suchen Keine gültigen Apothekeninformationen Es wurden keine aktuellen Informationen über diese Apotheke gefunden. Der Eintrag zu dieser Apotheke wird gelöscht. Okay @@ -567,8 +541,8 @@ Derzeit können keine aktuellen Informationen über diese Apotheke abgerufen werden. Bitte überprüfen Sie Ihre Internetverbindung. Abbrechen Erneut versuchen - Select Environment - Save Environment + Wählen Environment + Speichern Environment Anmeldung nicht möglich Es scheint, als hätten sich Ihre Merkmale für die biometrische Anmeldung geändert. Bitte melden Sie sich erneut mit Ihrer Gesundheitskarte an. Abbrechen @@ -581,17 +555,15 @@ Produktverbesserungen Anonyme Analyse Helfen Sie uns, diese App besser zu machen. Alle Nutzungsdaten werden anonym erhoben und dienen ausschließlich der Verbesserung des Nutzungserlebnisses. - App Sicherheit Persönliche Einstellungen Bedienungshilfen Produktverbesserungen Rezept hinzugefügt Rezept bereits vorhanden Ein Fehler beim Importieren ist aufgetreten - Kartenzugangsnummer + Zugangsnummer Löschen Gescanntes Rezept - Ersatzpräparat möglich PIN vergessen %s Rezept @@ -607,7 +579,7 @@ Sie können diese Entscheidung in den Systemeinstellungen jederzeit ändern. Fortfahren Akzeptieren - Diese App verwendet die sicherste Methode, die von Ihrem Gerät zur Verfügung gestellt wird. + Die App verwendet die sicherste Methode, die Sie auf Ihrem Gerät zur eingerichtet haben. Speichern Wählen Medikament @@ -636,15 +608,13 @@ Was ist eine Direktzuweisung? Bei einer Direktzuweisungen wird ein Rezept von einer Praxis oder einem Krankenhaus direkt bei einer Apotheke eingelöst. Versicherte müssen hierbei nicht tätig werden und können nicht in den Einlösungsprozess eingreifen.\n\nDirektzuweisungen werden in der E-Rezept App aufgeführt, um Ihre Behandlung für Sie transparenter zu machen. Notdienstgebühr - Manchmal ist Eile geboten. Einige Rezepte können ohne die zusätzliche Zahlung einer Notdienstgebühr eingelöst werden, beispielsweise nachts oder an Feiertagen. + Wird ein Rezept zwischen 20 Uhr abends und 6 Uhr morgens oder an Sonn- und Feiertagen eingelöst, kann eine zusätzliche Gebühr von 2,50 Euro erhoben werden. Zuzahlungspflichtige Medikamente Von der Zuzahlung befreit - Gesetzlich Versicherte müssen für verschreibungspflichtige Medikamente eine Zuzahlung von bis zu zehn Euro leisten.\n\nDie Höhe der Zuzahlung ist abhängig von dem Preis Ihres Medikaments. Medikamente, die weniger als 5€ kosten müssen Sie selbst zahlen.\nBei Medikamenten die teurer sind, müssen Sie zehn Prozent des Preises zuzahlen, mindestens aber 5€ und höchstens 10€.\n\nGrundsätzlich befreit von einer Zuzahlung sind Kinder und Jugendliche unter 18 Jahren. \n\nSollten Ihre jährlichen Kosten für Medikamente Ihre finanzielle Belastungsgrenzt überschreiten können Sie sich von der Zuzahlung befreien lassen. Sprechen Sie dazu mit Ihrer Krankenversicherung. + Gesetzlich Versicherte zahlen für verschreibungspflichtige Medikamente in der Regel höchstens 10 Euro. Höhere Gebühren können anfallen, wenn ein Arzneimittel von einem bestimmten Hersteller gewünscht wird, welches nicht von einem Rabattvertrag der Krankenkasse abgedeckt ist (\"Wunscharzneimittel\").\n\nKinder und Jugendliche unter 18 sind von Zuzahlungen befreit. \n\nWerden Rezepte später als 28 Tage nach Ausstellung eingelöst, müssen die Kosten vollständig selbst übernommen werden. \n\nBei hohen Medikamentenausgaben über das Jahr kann eine Befreiung von der Zuzahlung bei der Krankenkasse beantragt werden Sie sind von der Zuzahlung von diesem Medikament befreit. Die Kosten für das Medikament übernimmt Ihre Krankenkasse. Wie lange ist dieses Rezept gültig? Innerhalb dieses Zeitraums können Sie Ihr Rezept in einer beliebigen Apotheke mit einer Zuzahlung von maximal 10€ einlösen. - Ersatzpräparat möglich - Aufgrund gesetzlicher Vorgaben Ihrer Krankenkasse kann Ihnen eine Alternative mit dem selben Wirkstoff ausgehändigt werden.\n\nMedikamente können unterschiedlich aussehen und heißen, andere Preise und Hersteller haben, aber dennoch den gleichen Wirkstoff beinhalten. Für die Wirkung von Arzneimitteln im Körper sind vor allem der Wirkstoff selbst und die Dosierung ausschlaggebend. Oft bekommen Patienten und Patientinnen in der Apotheke ein anderes Medikament als das vom Arzt oder der Ärztin auf dem Rezept verordnete – vorausgesetzt, die Medikamente sind vergleichbar. Für den Wechsel kann es therapeutische und wirtschaftliche Gründe geben. Gescanntes Rezept Von einem Papierausdruck importierte Rezepte dürfen aus Sicherheitsgründen keine persönlichen oder medizinische Daten anzeigen.\n\nMelden Sie sich in dieser App mit Gesundheitskarte oder Versicherungs-App an, um alle im Rezept enthaltenen Informationen einzusehen. Rezept fehlerhaft @@ -654,7 +624,7 @@ Telefon Website Mail - Sortierung nach Entfernung nicht möglich. + Geben Sie Ihren Standort frei, um Apotheken in Ihrer Nähe zu finden. Okay Aktuelle PIN eingeben Falsche PIN eingegeben @@ -672,12 +642,10 @@ Gesundheitskarte Biometrie Nicht angemeldet - Wir sind an Ihrer Meinung interessiert. Bitte nehmen Sie sich fünf Minuten Zeit, um unsere Umfrage zu beantworten. Haben Sie vielen Dank im Voraus. Warnhinweis Apotheke zu Favoriten hinzugefügt Apotheke aus Favoriten entfernt Meine Apotheken - Kennwortstärke sehr gut Schreibvorgang nicht erfolgreich PIN konnte nicht gespeichert werden Melden @@ -696,7 +664,6 @@ Eingelöst am %s Eingelöst gerade eben Eingelöst um %s Uhr - Bestellungen Dieses Rezept wurde im Rahmen einer Behandlung für Sie eingelöst. Notdienstgebühr Dieses Rezept kann nachts in einer Apotheke nicht ohne zusätzliche Zahlung einer Notdienstgebühr eingelöst werden. @@ -710,7 +677,7 @@ Rezepte digital empfangen? Ziehen Sie den Screen nach unten, um zu aktualisieren. Keine Rezepte - Melden Sie sich an, um Rezepte automatisch zu erhalten oder fügen Sie über das ⊕ in der oberen Ecke ein neues Rezept hinzu. + Melden Sie sich an, um Rezepte automatisch zu erhalten. Anmelden Rezeptarchiv Vielleicht später @@ -747,9 +714,9 @@ Klicken Sie auf das Display um den angezeigten Tooltip zu überspringen. Wie einlösen? Wie möchten Sie Ihre Medikamente erhalten? - Direkt einlösen - Medikamente vor Ort einlösen - Bestellen + Code vorzeigen + In der Apotheke scannen lassen + An Apotheke senden Reservieren oder liefern lassen Fertig Sammelcode @@ -815,7 +782,7 @@ Hierdurch werden alle Kostenbelege von diesem Gerät und vom Server gelöscht. Kostenbelege empfangen Ihre Kostenbelege werden zusätzlich auf dem Rezeptserver gespeichert. - Empfangen + Aktivieren Gesamt: %s %s Auswählen Teilen @@ -869,17 +836,17 @@ Zugehörige PIN benötigt Nur noch morgen als Selbstzahlender einlösbar Nur noch %s Tage als Selbstzahlender einlösbar - \n Noch %s Tage als Selbstzahlender einlösbar\n - Nur noch %s Tage gültig - \n Noch %s Tage gültig\n - Nur noch morgen gültig + Noch %s Tage als Selbstzahlender einlösbar + Nur noch %s Tage einlösbar + Noch %s Tage einlösbar + Nur noch morgen einlösbar Gebührenpflichtig Übernimmt Versicherung Das Rezept/die Rezepte wurden erfolgreich übertragen. Das Rezept kann nicht verarbeitet werden. Bitte versuchen Sie es erneut. Eventuell müssen Sie eine andere Apotheke auswählen. Das Rezept kann nicht verarbeitet werden. Die Apotheke meldet einen unbekannten Fehler. Probieren Sie ggf. eine andere Apotheke. Das Rezept wurde durch die Apotheke abgelehnt. Möglicherweise ist das Rezept ungültig, oder Ihre Lieferadresse bzw. Kontaktdaten ungültig. - Einlösen nicht möglich, bitte überprüfen Sie Ihre Internetverbindung. + Versuchen Sie es erneut und wählen Sie eventuell eine andere Apotheke. Sollte der Fehler weiterhin bestehen, informieren Sie bitte den Support. Das Rezept wurde erfolgreich übertragen. Die Apotheke meldet aber einen Verarbeitungsfehler. Bitte kontaktieren Sie die Apotheke. Das Rezept wurde durch die Apotheke abgelehnt. Das Rezept wurde bereits eingelöst. Das Rezept wurde durch die Apotheke abgelehnt. Das Rezept wurde gelöscht. @@ -913,8 +880,7 @@ Jetzt nicht Suche Sie haben einen Kostenbeleg zu Ihrem Medikament %s erhalten. - "Kostenbeleg anzeigen" - NFC-fähiges Smartphone erforderlich + "Kostenbeleg anzeigen." Kostenbelege digital erhalten Nach Aktivierung der Kostenbelege finden Sie diese nach der Einlösung Ihres Rezepts hier. Aktivieren @@ -923,11 +889,11 @@ Kostenbeleg \uD83D\uDCA1 Kostenbelege digital erhalten - Hinweis: Sie erhalten Ihre Kostenbelege nicht mehr als Ausdruck in der\n Apotheke.\n + Hinweis: Sie erhalten Ihre Kostenbelege nicht mehr als Ausdruck in der Apotheke. Aktivieren Vielleicht später Einverständnis - Mit Ihrem Einverständnis verzichten Sie auf den Ausdruck in der Apotheke\n und reichen Ihre Kostenbelege künftig digital ein.\n + Mit Ihrem Einverständnis verzichten Sie auf den Ausdruck in der Apotheke und reichen Ihre Kostenbelege künftig digital ein. Abbrechen Einverstanden Demo-Modus Beenden @@ -942,17 +908,17 @@ Kein Internet Es besteht keine Verbindung zum Internet. Fehlerhafte Anfrage - Es gab ein Problem mit der Anfrage. Wir arbeiten an einer Lösung.\n + Es gab ein Problem mit der Anfrage. Wir arbeiten an einer Lösung. Server antwortet nicht Bitte probieren Sie es in einigen Minuten erneut. Herstellen der Verbindung fehlgeschlagen - Derzeit können keine Informationen abgerufen werden. Bitte\n probieren Sie es später erneut.\n + Derzeit können keine Informationen abgerufen werden. Bitte probieren Sie es später erneut. Abgelehnt - Der Server hat Ihre Anfrage abgelehnt. Bitte probieren Sie es\n später erneut.\n + Der Server hat Ihre Anfrage abgelehnt. Bitte probieren Sie es später erneut. App aktualisieren - Um diese Funktion nutzen zu können, aktualisieren Sie bitte Ihre App.\n + Um diese Funktion nutzen zu können, aktualisieren Sie bitte Ihre App. Anmeldung fehlgeschlagen - Der Server hatte ein Problem bei der Anmeldung. Bitte melden Sie sich\n erneut an.\n + Der Server hatte ein Problem bei der Anmeldung. Bitte melden Sie sich erneut an. Anmelden Copy URL Verbindung mit externer Krankenversicherungs-App herstellen. @@ -980,15 +946,309 @@ Fehlerhafte Daten - Korrektur erforderlich Zusätzliche App benötigt Empfohlen - Digitale Gesundheits-ID - Einstellungen anpassen ? - 404 Seite? + Digitale GesundheitsID + Einstellungen anpassen? Bitte aktivieren Sie in Ihren Einstellungen das Öffnen von Links, um fortzufahren. Einstellungen öffnen Veraltete Appversion Ihre Version der App ist veraltet und wird nicht mehr unterstützt. Um das E-Rezept weiterhin nutzen zu können, aktualisieren Sie bitte die App. Aktualisieren https://play.google.com/store/apps/details?id=de.gematik.ti.erp.app - Ihre CAN ist %s Ziffern lang. + Ihre Zugangsnummer ist %s Ziffern lang. Ihre PIN kann %s bis %s Ziffern lang sein. + Abholung + Bote + Versand + NFC-fähiges Smartphone erforderlich + Wir konnten die Anfrage zum Löschen des Rezeptes nicht verarbeiten. Wir arbeiten an einer Lösung (%s). + Beim Löschen meines Rezeptes ist etwas schiefgegangen, Ich habe den FehlerCode %s erhalten. Bitte benachrichtigen Sie mich, wenn das Löschen wieder möglich ist. + Weiter + Fehlerhafte Anfrage + Melden + Lokal löschen + Kein Internet + Das Rezept konnte nicht gelöscht werden. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut. + Erneut probieren + OK + Erneut anmelden + Sie müssen angemeldet sein, um das Rezept zu löschen (401). + Anmelden + Abbrechen + Löschen unmöglich + Sie dürfen das Rezept derzeit nicht löschen, da es bei der Apotheke zur Einlösung liegt oder es eine ausstehende Direktzuweisung ist (403). + Zu viele Anfragen + Sie haben zu oft probiert, das Rezept zu löschen. Bitte probieren Sie es später erneut (429). + Rezept löschen + Uuups... + "Es ist ein unerwarteter Serverfehler aufgetreten. Bitte versuchen Sie es später erneut (500). " + Appabsicherung nicht möglich. Richten Sie zuvor auf Ihrem Gerät eine biometrische Absicherung (z.B. Fingerabruck) ein. + Für dieses Gerät ist keine biometrische Sicherung möglich, da dies vom Ihrem Gerät nicht unterstützt wird. + Achtung! Ihr Telefon ist nicht gut geschützt. + Ihre Zugangsdaten werden auf Ihrem Telefon gespeichert. Um den Zugriff zu schützen, wird Ihre bevorzugte Anmeldemethode verwendet. Diese Anmeldemethode, z.B. Wischmuster, ist nicht sehr sicher. Sie nutzen die Funktion \"Anmeldedaten speichern\" auf eigenes Risiko. + Wenn Sie die Funktion \"Anmeldedaten speichern\" dennoch nutzen, können Sie mit der E-Rezept App in Zukunft ohne Gesundheitskarte und Eingabe der PIN Rezepte einsehen, abrufen, einlösen oder löschen. + Standort nicht gefunden + An Apotheke senden + Code vorzeigen + Sprache + Sprache + Deutsch + Arabisch + Bulgarisch + Tschechisch + Dänisch + Englisch + Französisch + Hebräisch + Italienisch + Niederländisch + Polnisch + Rumänisch + Russisch + Türkisch + Ukrainisch + Standard + Digitale Kostenbelege wurden deaktiviert und gelöscht. + https://www.das-e-rezept-fuer-deutschland.de/ext/community + "Forum zum E-Rezept " + Kein Ersatzpräparat möglich + Ersatzpräparat möglich + Ersatzpräparat (Aut idem) + Apothekerinnen und Apotheker sind verpflichtet, vorrangig Arzneien abzugeben, für welche die Krankenkasse des Patienten einen Rabattvertrag mit Arzneimittelherstellern abgeschlossen hat. Dies gilt nur dann nicht, wenn der Arzt oder die Ärztin \"Aut idem\" auf dem Rezept ausschließt, was bei Ihrem Rezept nicht der Fall ist. + Ihre Ärztin bzw. Ihr Arzt hat festgelegt, dass Sie das verordnete Präparat erhalten sollen. Die Apotheke soll keinen Austausch auf Basis eines Rabattvertrages vornehmen (“Aut idem”). + Screenshots erlauben + Wenn Sie Screenshots erlauben, bleibt bei einem App-Wechsel die letzte geöffnete Seite im Hintergrund sichtbar. Gegebenenfalls sind dadurch Ihre persönlichen Daten zu sehen. Wir empfehlen deshalb, Screenshots nicht zu erlauben. + Erlauben + Abbrechen + Wechsel deine Tastatur zu Stickern oder verwende Emojis für dein Profilbild. + Profilbild wählen + Wie möchten Sie fortfahren? + Foto auswählen + Kamera + Emoji + Abbrechen + Einstellungen öffnen + Verwenden + Foto aufnehmen + Profilbild bearbeiten + Angenommen vor %s Minuten + Angenommen am %s + Angenommen gerade eben + Angenommen um %s Uhr + GesundheitsID + Versicherung wählen + Wenn die Anmeldung mit der GesundheitsID nicht wie erwartet verläuft, folgen Sie bitte den Tipps aus unserer Hilfe. + Hilfe + Hilfe + Tipps zur Anmeldung mit der Versicherungsapp + Für die GesundheitsID ist Ihre Versicherung zuständig. Kontaktieren Sie diese bitte bei Fragen zur Anmeldung. Hier einige bewährte Tipps: + Bitte beachten Sie, dass je nach Versicherung eine separate App benötigt wird. Bitte erkundigen Sie sich bei Ihrer Versicherung welche dies ist. + Starten Sie die Kassen-App und melden Sie sich dort einmal an, bevor Sie die Anmeldung in der E-Rezept App starten. + Ein Wechsel zwischen der Anmeldung mit der GesundheitsID und der Gesundheitskarte kann zu Problemen führen. Melden Sie sich daher bitte zunächst aktiv von Ihrem Profil ab, bevor Sie die Anmeldevariante ändern. + Falls Ihre Versicherung nicht in der Liste aufgeführt ist, können Sie sich alternativ mit Ihrer Gesundheitskarte und der dazugehörigen PIN anmelden. + Wenn Sie von der App der Kasse nicht zurück in die E-Rezept App geleitet werden, melden Sie diesen Fehler bitte unbedingt Ihrer Versicherung. + Falls die Versicherungsapp nicht aufgerufen werden kann, kann es hilfreich sein, in die Browser Einstellungen Ihres Smartphones zu gehen und das Öffnen von Links zu erlauben. + Wenn Ihr Smartphone mit Android 14 betrieben wird, könnte es hilfreich sein, in den Einstellungen das Öffnen von Links zu erlauben. + Einstellungen öffnen + Das hat leider nicht geklappt + Bitte probieren Sie es zu einem späteren Zeitpunkt erneut. + Fehlermeldungen beim Laden. + Bitte wählen Sie ein Emoji oder einen Text + Speichern + Zum Fortfahren bitte anmelden + Weiter + Anrufen + Mail \nSchreiben + Zur Apotheke + Apotheke + Heute + Morgen + Route\nhierhin + Rezept und Kostenbeleg löschen + Wenn Sie das Rezept löschen, wird der verbundene Kostenbeleg ebenfalls gelöscht. + Die Mitteilungen aus allen Profilen werden jetzt zusammen angezeigt + Neue Features + Neue Features + Wir arbeiten ständig an neuen Features. Teilen Sie uns Ihre Meinung mit und helfen Sie uns, eine bessere App zu entwickeln. + Bestellungen für alle + Unter Bestellungen können Sie jetzt die Bestellungen für alle Profile zusammen sehen und sie viel einfacher einsehen + + Keine Kostenübernahme + Für dieses Rezept erfolgt in der Regel keine Kostenübernahme durch die Krankenversicherung. Als Patient*in sind Sie daher für die vollständige Bezahlung verantwortlich. Ob die Kosten im Rahmen einer Zusatzversicherung oder Satzungsleistung erstattet werden, ist individuell zu prüfen. + Für dieses Rezept übernimmt Ihre Versicherung keine Kosten. + + "Für das Rezept %s übernimmt Ihre Versicherung keine Kosten" + "Für die Rezepte %s übernimmt Ihre Versicherung keine Kosten." + + Ursache + Datum + Unfall + Arbeitsunfall + Betriebskrankheit + Nachrichten + Keine Nachrichten + Sie haben noch keine Nachrichten. + 🎉 Ihre Bestellung liegt zur Abholung bereit. Bitte zeigen Sie diesen Abholcode vor, um sich auszuweisen. + Leider war die Nachricht Ihrer Apotheke leer. Bitte kontaktieren Sie Ihre Apotheke. + Die Apotheke hat Ihnen einen Link zur Verfügung gestellt. + Nachrichten + Bestellung + Nachrichten + Keine Internetverbindung + Um die verbundenen Geräte anzuzeigen, müssen Sie mit dem Biometrie verbunden sein. + Rezept wurde am %s gelöscht + Gelöscht + Keine Rezepte + Sie haben keine Rezepte im Archiv. + Ihre Kostenbelege wurde gelöscht + Kostenbeleg + Anmeldung notwendig + Bitte melden Sie sich an, um die verbundenen Geräte anzuzeigen. + Anmelden + Anmelden + Verbinden mit %s Versicherung + Gesundheitskarte am Smartphone + Nicht angemeldet + Angemeldet + refresh + Sie erhalten Zugriffsprotokolle, wenn Sie am Rezeptdienst angemeldet sind. + Anmeldung notwendig + Bitte melden Sie sich mit Ihrer Gesundheitskarte an und speichern Sie Ihre Anmeldedaten mit Biometrie, um die verbundenen Geräte anzuzeigen. + Keine Erinnerung eingerichtet + Erinnerung aktiv + Erinnerung inaktiv + Einnahmeerinnerung + %1$sx Morgens + %1$sx Mittags + %1$sx Abends + %1$sx Nachts + Sofern von Ihrem Arzt oder Ihrer Ärztin keine abweichenden Anweisungen vorliegen, können die Einnahmehinweise wie folgt verstanden werden: + Ihr Arzt oder Ihre Ärztin hat Ihnen diese Informationen zur Einnahme des Medikaments mitgegeben. + In Ihrem Rezept liegen keine Informationen zum Einnahmehinweis vor. + Ihr Arzt oder Ihre Ärztin hat notiert, dass Sie eine Anweisung zur Einnahme erhalten haben, die nicht auf dem Rezept vermerkt ist. Diese könnte zum Beispiel auf Ihrem Medikationsplan stehen. + Keine Angabe + DJ + Einnahmehinweis + " %s - %s" + Einnahmererinnerung + Einnahmeerinnerungen + Medikamente zum Einnehmen + Zu den Einnahmeerinnerungen + Morgens + Mittags + Nachmittags + Abends + Medikament + Dosierungs-Angaben + 1 + %s / %s + 1 Dosis + Dosierung ändern + Abbrechen + Medikamenten-Erinnerung + Medikamentenplan + Nehmen Sie die Dosierung gemäß ärztlicher Verordnung + Um %s Uhr %s x nehmen + Termine auswählen + Weiter + Stornieren + Ein + Aus + Tracking + Unbegrenzt + Individuell + Erster Tag + Letzter Tag + Uhrzeit hinzufügen + Keine Einnahmeerinnerungen + Sie können Einnahmeerinnerungen für Ihre Rezepte einstellen + Ein + Aus + Erinnere mich + Batterieoptimierung für diese App ausschalten. + Wenn du diese Option nicht aktivierst, kann es passieren, dass die Erinnerungen an die Medikamenteneinnahme unzuverlässig angezeigt werden. + Abbrechen + Erlauben + Einnahmehinweis + Keine Angabe + DJ + Information + Erinnere mich auch im Batterieoptimierungsmodus + Dosierung ändern + Menge + Form + beendet + unbegrenzt + bis %s + Wiederholen %s + Wenn möglich, nutzen wir für die Berechnung die im Rezept gespeicherten Informationen. + Abbrechen + Speichern + Bis zum Packungsende + Zeit + Dosis + 1 + Medikamente anzeigen + Haben Sie Ihre Medikamente bereits eingenommen? + Einnahmeerinnerungen + Keine aktiven Erinnerungen + Sie haben heute keine Einnahmeerinnerungen + Startdatum auswählen + Enddatum auswählen + Herzlich willkommen + in Ihrer App für E-Rezepte + Erneut versuchen + Passwort eingeben + Bitte achten Sie darauf, dass Personen, mit denen Sie gegebenenfalls dieses Gerät teilen und die Ihre Anmeldedaten kennen, ebenfalls Zugriff auf Ihre Rezepte erhalten. + Passwort eingeben + Bitte geben Sie das Passwort zum Entsperren der App ein. + Passwort + Passwort + Bitte geben Sie ein Passwort zur Sicherung der App ein. + Passwort anzeigen lassen + Passwort wiederholen + Passwort vergessen? Bitte löschen Sie die App und installieren sie anschließend erneut. Weshalb das so ist, erfahren Sie in unserem %s. + Passwort eingeben + Das Passwort muss mindestens acht Zeichen lang sein + Passwortstärke nicht ausreichend + Passwortstärke ausreichend + Gerätesicherung nicht möglich + Bitte richten Sie zuerst eine Gerätesicherung ein. + Abbrechen + Einstellungen + Passwortstärke sehr gut + App Sicherheit + Gerätesicherung + Bitte wählen Sie mindestens eine Methode zur Sicherung der App. + Passwort + Passwort ändern + Zum Löschen muss eine Verbindung zum Rezeptserver hergestellt werden + Ware steht bereit + Ware steht bereit seit %s + Ware steht bereit seit gerade eben + Ware steht bereit seit %s Minuten + Ware steht bereit seit %s + Deaktiviert, weil kein Passwort eingegeben ist. + Deaktiviert, weil das Passwort zu schwach ist. + Deaktiviert, weil die Passwörter nicht übereinstimmen. + Erkunden + Organspenderegister + Organspenderegister öffnen? + Sie werden zum Organspenderegister weitergeleitet. Um Ihre Daten zur Organspende einsehen und ändern zu können, müssen Sie sich dort anmelden. + Öffnen + Abbrechen + Funktion im Demo-Modus nicht aktiv + E-Rezept App Team + Neuerungen in der App Version %s + Herzlich Willkommen in der E-Rezept App! Mit dieser App können Sie digital E-Rezepte empfangen und an eine Apotheke Ihrer Wahl senden. Ob Sie das Rezept dort zur Abholung reservieren oder nach Hause liefern lassen möchten, liegt ganz bei Ihnen. Selbstverständlich können Sie auch die Rezepte Ihrer Familie verwalten. Um Rezepte digital zu erhalten, ist eine Anmeldung erforderlich.\n\nSchön, dass Sie da sind!\nIhr E-Rezept App Team + 🎉 Herzlich Willkommen! + Zu Favoriten hinzufügen + aktiv + inaktiv + Filter aktiv + Google Maps + Apothekensuche + Sucheingabe löschen + ASK-Nummer + ATC-Nummer + Snomed diff --git a/app/features/src/main/res/xml/locale_config.xml b/app/features/src/main/res/xml/locale_config.xml new file mode 100644 index 00000000..001650a9 --- /dev/null +++ b/app/features/src/main/res/xml/locale_config.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/features/src/main/res/xml/network_security_config.xml b/app/features/src/main/res/xml/network_security_config.xml index 40148399..b8f65835 100644 --- a/app/features/src/main/res/xml/network_security_config.xml +++ b/app/features/src/main/res/xml/network_security_config.xml @@ -1,98 +1,11 @@ - - - erp-dev.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4= - - - - - - erp-ref.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4= - - - - - - erp-test.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4= - - - - - - erp.app.ti-dienste.de - - RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4= - - - - - - idp-ref.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - - idp-test.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - - idp.app.ti-dienste.de - - 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= - OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= - - - - - apovzd.app.ti-dienste.de - - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - - - apovzd-test.app.ti-dienste.de - - e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= - qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= - - - + + + + diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt index ed05ed2f..e50ab6d8 100644 --- a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di @@ -23,6 +23,9 @@ import androidx.core.content.edit import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.debugsettings.data.Environment +/** + * Documentation: documentation-internal/variants/build_variants.adoc + */ class EndpointHelper( private val networkPrefs: SharedPreferences ) { @@ -57,12 +60,17 @@ class EndpointHelper( url = networkPrefs.getString( uri.preferenceKey, uri.original - )!! + ) ?: "" } - if (url.last() != '/') { - url += '/' + return when { + url.isEmpty() -> "https://github.com/gematik/E-Rezept-App-Android/" + url.last() != '/' -> { + url += '/' + url + } + + else -> return url } - return url } private fun overrideSwitchKey(uri: EndpointUri): String { @@ -80,6 +88,7 @@ class EndpointHelper( } } + @Suppress("CyclomaticComplexMethod") fun getCurrentEnvironment(): Environment { return when { eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_PU && @@ -87,32 +96,46 @@ class EndpointHelper( pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_PU -> { Environment.PU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.RU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU_DEV && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU_DEV && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.RUDEV } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TU && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TU && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.TU } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TR && idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TR && pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { Environment.TR } + else -> { return Environment.PU } } } + fun getOrganDonationRegisterIntentHost() = + if (getCurrentEnvironment() == Environment.PU) { + BuildKonfig.ORGAN_DONATION_REGISTER_PU + } else { + BuildKonfig.ORGAN_DONATION_REGISTER_RU + } + + fun getOrganDonationRegisterInfoHost() = BuildKonfig.ORGAN_DONATION_INFO + fun getErpApiKey(): String { return if (BuildKonfig.INTERNAL) { when (getCurrentEnvironment()) { diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt index e2ee35eb..4b16ac26 100644 --- a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt @@ -1,25 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.ui import android.content.Intent import android.net.Uri +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -45,13 +46,15 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Switch import androidx.compose.material.Text -import androidx.compose.material.TextField import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -74,25 +77,32 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.debugsettings.logger.ui.screens.LoggerScreen import de.gematik.ti.erp.app.debugsettings.navigation.DebugScreenNavigation +import de.gematik.ti.erp.app.debugsettings.pkv.ui.DebugScreenPKV +import de.gematik.ti.erp.app.debugsettings.qrcode.QrCodeScannerScreen import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen -import de.gematik.ti.erp.app.debugsettings.ui.DebugScreenPKV -import de.gematik.ti.erp.app.debugsettings.ui.EnvironmentSelector -import de.gematik.ti.erp.app.debugsettings.ui.LoadingButton +import de.gematik.ti.erp.app.debugsettings.ui.components.ClearTextTrafficSection +import de.gematik.ti.erp.app.debugsettings.ui.components.ClientIdsSection +import de.gematik.ti.erp.app.debugsettings.ui.components.EnvironmentSelector +import de.gematik.ti.erp.app.debugsettings.ui.components.LoadingButton import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.ui.LabelButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium +import de.gematik.ti.erp.app.utils.SpacerSmall import de.gematik.ti.erp.app.utils.compose.AlertDialog import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ErezeptOutlineText +import de.gematik.ti.erp.app.utils.compose.LabelButton import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.navigationModeState +import de.gematik.ti.erp.app.utils.extensions.erezeptColors import kotlinx.coroutines.launch import org.bouncycastle.util.encoders.Base64 +import org.kodein.di.Copy import org.kodein.di.bindProvider import org.kodein.di.compose.rememberViewModel import org.kodein.di.compose.subDI @@ -101,7 +111,7 @@ import java.io.ByteArrayOutputStream import java.time.LocalDateTime import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream - +@file: Suppress("UnusedPrivateMember", "MagicNumber") @Composable fun DebugCard( modifier: Modifier = Modifier, @@ -182,7 +192,7 @@ fun EditablePathComponentWithControl( content: @Composable ((Boolean) -> Unit) -> Unit ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) { - TextField( + ErezeptOutlineText( value = textFieldValue, onValueChange = { onValueChange(it, false) }, label = { Text(label) }, @@ -203,26 +213,30 @@ fun DebugScreen( val navController = rememberNavController() val navMode by navController.navigationModeState(DebugScreenNavigation.DebugMain.path()) - subDI(diBuilder = { - bindProvider { - DebugSettingsViewModel( - visibleDebugTree = instance(), - endpointHelper = instance(), - cardWallUseCase = instance(), - prescriptionUseCase = instance(), - invoiceRepository = instance(), - vauRepository = instance(), - idpRepository = instance(), - idpUseCase = instance(), - profilesUseCase = instance(), - featureToggleManager = instance(), - pharmacyDirectRedeemUseCase = instance(), - getAppUpdateManagerFlagUseCase = instance(), - changeAppUpdateManagerFlagUseCase = instance(), - dispatchers = instance() - ) + subDI( + copy = Copy.All, + diBuilder = { + bindProvider { + DebugSettingsViewModel( + visibleDebugTree = instance(), + endpointHelper = instance(), + cardWallUseCase = instance(), + prescriptionUseCase = instance(), + saveInvoiceUseCase = instance(), + vauRepository = instance(), + idpRepository = instance(), + idpUseCase = instance(), + profilesUseCase = instance(), + featureToggleManager = instance(), + pharmacyDirectRedeemUseCase = instance(), + getAppUpdateManagerFlagUseCase = instance(), + changeAppUpdateManagerFlagUseCase = instance(), + dispatchers = instance() + ) + } } - }) { + ) { + val viewModel by rememberViewModel() NavHost( navController, startDestination = DebugScreenNavigation.DebugMain.path() @@ -230,6 +244,7 @@ fun DebugScreen( composable(DebugScreenNavigation.DebugMain.route) { NavigationAnimation(mode = navMode) { DebugScreenMain( + viewModel = viewModel, onBack = { settingsNavController.popBackStack() }, @@ -241,6 +256,12 @@ fun DebugScreen( }, onClickBioMetricSettings = { navController.navigate(DebugScreenNavigation.DebugTimeout.path()) + }, + onScanQrCode = { + navController.navigate(DebugScreenNavigation.QrCodeScannerScreen.path()) + }, + onClickLogger = { + navController.navigate(DebugScreenNavigation.LoggerScreen.path()) } ) } @@ -248,6 +269,7 @@ fun DebugScreen( composable(DebugScreenNavigation.DebugRedeemWithoutFD.route) { NavigationAnimation(mode = navMode) { DebugScreenDirectRedeem( + viewModel = viewModel, onBack = { navController.popBackStack() } @@ -255,7 +277,6 @@ fun DebugScreen( } } composable(DebugScreenNavigation.DebugPKV.route) { - val viewModel by rememberViewModel() NavigationAnimation(mode = navMode) { DebugScreenPKV( onSaveInvoiceBundle = { @@ -272,13 +293,31 @@ fun DebugScreen( navController.popBackStack() } } + composable(DebugScreenNavigation.QrCodeScannerScreen.route) { + QrCodeScannerScreen.Content( + onSaveCertificate = { viewModel.onSetVirtualHealthCardCertificate(it) }, + onSavePrivateKey = { viewModel.onSetVirtualHealthCardPrivateKey(it) }, + onBack = { + navController.popBackStack() + } + ) + } + composable(DebugScreenNavigation.LoggerScreen.route) { + LoggerScreen.Content( + onBack = { + navController.popBackStack() + } + ) + } } } } @Composable -fun DebugScreenDirectRedeem(onBack: () -> Unit) { - val viewModel by rememberViewModel() +fun DebugScreenDirectRedeem( + viewModel: DebugSettingsViewModel, + onBack: () -> Unit +) { val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -307,7 +346,7 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Endpoints" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = shipmentUrl, label = { Text("Shipment URL") }, @@ -315,7 +354,7 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { shipmentUrl = it } ) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = deliveryUrl, label = { Text("Delivery URL") }, @@ -323,7 +362,7 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { deliveryUrl = it } ) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier.fillMaxWidth(), value = onPremiseUrl, label = { Text("OnPremise URL") }, @@ -360,7 +399,7 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Message" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .heightIn(max = 400.dp) .fillMaxWidth(), @@ -376,7 +415,7 @@ fun DebugScreenDirectRedeem(onBack: () -> Unit) { DebugCard( title = "Certificates" ) { - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .heightIn(max = 400.dp) .fillMaxWidth(), @@ -410,12 +449,14 @@ private fun RedeemButton( @OptIn(ExperimentalMaterialApi::class) @Composable fun DebugScreenMain( + viewModel: DebugSettingsViewModel, onBack: () -> Unit, onClickDirectRedemption: () -> Unit, onClickPKV: () -> Unit, - onClickBioMetricSettings: () -> Unit + onClickBioMetricSettings: () -> Unit, + onScanQrCode: () -> Unit, + onClickLogger: () -> Unit ) { - val viewModel by rememberViewModel() val listState = rememberLazyListState() val modal = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() @@ -483,6 +524,12 @@ fun DebugScreenMain( ) { viewModel.refreshPrescriptions() } + LabelButton( + icon = Icons.Rounded.Adb, + text = "Logger" + ) { + onClickLogger() + } } } item { @@ -500,6 +547,7 @@ fun DebugScreenMain( ) Switch( modifier = Modifier.testTag(TestTag.DebugMenu.FakeAppUpdate), + colors = SwitchDefaults.erezeptColors(), checked = appUpdateManager, onCheckedChange = { viewModel.changeAppUpdateManager(it) } ) @@ -516,11 +564,11 @@ fun DebugScreenMain( ) { Text( text = "Fake NFC Capability", - modifier = Modifier - .weight(1f) + modifier = Modifier.weight(1f) ) Switch( modifier = Modifier.testTag(TestTag.DebugMenu.FakeNFCCapabilities), + colors = SwitchDefaults.erezeptColors(), checked = viewModel.debugSettingsData.fakeNFCCapabilities, onCheckedChange = { viewModel.allowNfc(it) } ) @@ -565,14 +613,30 @@ fun DebugScreenMain( } } item { - VirtualHealthCard(viewModel = viewModel) + VirtualHealthCard( + viewModel = viewModel + ) { + onScanQrCode() + } } - item { + /*item { FeatureToggles(viewModel = viewModel) - } + }*/ item { RotatingLog(viewModel = viewModel) } + item { + HorizontalDivider() + } + item { + ClearTextTrafficSection() + } + item { + HorizontalDivider() + } + item { + ClientIdsSection() + } } } } @@ -614,7 +678,11 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV } @Composable -private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { +private fun VirtualHealthCard( + modifier: Modifier = Modifier, + viewModel: DebugSettingsViewModel, + onScanQrCode: () -> Unit +) { var virtualHealthCardLoading by remember { mutableStateOf(false) } var virtualHealthCardError by remember { mutableStateOf(null) } virtualHealthCardError?.let { error -> @@ -635,8 +703,19 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet DebugCard(modifier, title = "Virtual Health Card", onReset = viewModel::onResetVirtualHealthCard) { val scope = rememberCoroutineScope() + TextButton( + modifier = Modifier.fillMaxWidth(), + border = BorderStroke(1.dp, AppTheme.colors.primary600), + shape = RoundedCornerShape(8.dp), + onClick = onScanQrCode + ) { + Text( + text = "Scan Virtual Health Card", + color = AppTheme.colors.primary600 + ) + } - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .testTag(TestTag.DebugMenu.CertificateField) .heightIn(max = 144.dp) @@ -654,7 +733,7 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet } Text(subjectInfo, style = AppTheme.typography.caption1l) - OutlinedTextField( + ErezeptOutlineText( modifier = Modifier .testTag(TestTag.DebugMenu.PrivateKeyField) .heightIn(max = 144.dp) diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt index 76c0e894..f29baca7 100644 --- a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.ui diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt index 5e0a2569..b1cdfccd 100644 --- a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.ui @@ -36,7 +36,7 @@ import de.gematik.ti.erp.app.VisibleDebugTree import de.gematik.ti.erp.app.appupdate.usecase.ChangeAppUpdateManagerFlagUseCase import de.gematik.ti.erp.app.appupdate.usecase.GetAppUpdateManagerFlagUseCase import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase -import de.gematik.ti.erp.app.data.DebugSettingsData +import de.gematik.ti.erp.app.debugsettings.data.DebugSettingsData import de.gematik.ti.erp.app.debugsettings.data.Environment import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager @@ -45,7 +45,7 @@ import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.AccessToken import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.invoice.usecase.SaveInvoiceUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -83,9 +83,9 @@ class DebugSettingsViewModel( private val endpointHelper: EndpointHelper, private val cardWallUseCase: CardWallUseCase, private val prescriptionUseCase: PrescriptionUseCase, + private val saveInvoiceUseCase: SaveInvoiceUseCase, private val vauRepository: VauRepository, private val idpRepository: IdpRepository, - private val invoiceRepository: InvoiceRepository, private val idpUseCase: IdpUseCase, private val profilesUseCase: ProfilesUseCase, private val featureToggleManager: FeatureToggleManager, @@ -131,9 +131,9 @@ class DebugSettingsViewModel( updateState( debugSettingsData.copy( cardAccessNumberIsSet = ( - cardWallUseCase.authenticationData(it) - .first().singleSignOnTokenScope as? IdpData.TokenWithHealthCardScope - )?.cardAccessNumber?.isNotEmpty() + cardWallUseCase.authenticationData(it) + .first().singleSignOnTokenScope as? IdpData.TokenWithHealthCardScope + )?.cardAccessNumber?.isNotEmpty() ?: false, activeProfileId = it, bearerToken = idpRepository.decryptedAccessToken(it).first()?.accessToken ?: "" @@ -146,10 +146,10 @@ class DebugSettingsViewModel( } fun selectEnvironment(environment: Environment) { - updateState(getDebugSettingsdataForEnvironment(environment)) + updateState(getDebugSettingsDataForEnvironment(environment)) } - private fun getDebugSettingsdataForEnvironment(environment: Environment): DebugSettingsData { + private fun getDebugSettingsDataForEnvironment(environment: Environment): DebugSettingsData { return when (environment) { Environment.PU -> debugSettingsData.copy( eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_PU, @@ -241,9 +241,10 @@ class DebugSettingsViewModel( else -> it } idpRepository.saveSingleSignOnToken( - activeProfileId, - newToken + profileId = activeProfileId, + token = newToken ) + idpRepository.invalidateDecryptedAccessToken(activeProfileId) Napier.d("SSO token is now: $newToken", tag = "Debug Settings") } } @@ -388,7 +389,8 @@ class DebugSettingsViewModel( viewModelScope.launch { val profileId = profilesUseCase.activeProfileId().first() val bundle = Json.parseToJsonElement(invoiceBundle) - invoiceRepository.saveInvoice(profileId, bundle) + saveInvoiceUseCase(profileId, bundle) + // invoiceRepository.saveInvoice(profileId, bundle) } } diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt index f358586a..3f0074e5 100644 --- a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils.compose @@ -32,19 +32,12 @@ import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.BugReport import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.key -import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints @@ -54,7 +47,8 @@ import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import java.util.UUID +import de.gematik.ti.erp.app.utils.SpacerSmall +import de.gematik.ti.erp.app.utils.SpacerTiny @Composable fun OutlinedDebugButton( @@ -76,25 +70,6 @@ fun OutlinedDebugButton( } } -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.visualTestTag(tag: String) = - composed(fullyQualifiedName = "de.gematik.ti.erp.app.utils.compose.visualTestTag", key1 = tag) { - val activity = LocalContext.current as MainActivity - val uuid = remember { UUID.randomUUID().toString() } - - DisposableEffect(tag) { - onDispose { - activity.elementsUsedInTests -= uuid - } - } - - Modifier - .testTag(tag) - .onGloballyPositioned { - activity.elementsUsedInTests += uuid to MainActivity.ElementForTest(it.boundsInRoot(), tag) - } - } - @Composable fun DebugOverlay(elements: Map) { Box(Modifier.fillMaxSize()) { diff --git a/app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt index 54f9c0de..90a48ebc 100644 --- a/app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt +++ b/app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di @@ -22,6 +22,9 @@ import android.content.SharedPreferences import androidx.core.content.edit import de.gematik.ti.erp.app.BuildKonfig +/** + * Documentation: documentation-internal/variants/build_variants.adoc + */ class EndpointHelper( private val networkPrefs: SharedPreferences ) { @@ -56,12 +59,17 @@ class EndpointHelper( url = networkPrefs.getString( uri.preferenceKey, uri.original - )!! + ) ?: "" } - if (url.last() != '/') { - url += '/' + return when { + url.isEmpty() -> "https://github.com/gematik/E-Rezept-App-Android/" + url.last() != '/' -> { + url += '/' + url + } + + else -> return url } - return url } private fun overrideSwitchKey(uri: EndpointUri): String { @@ -90,4 +98,10 @@ class EndpointHelper( fun getTrustAnchor(): String = BuildKonfig.APP_TRUST_ANCHOR_BASE64 + + fun getOrganDonationRegisterIntentHost() = + BuildKonfig.ORGAN_DONATION_REGISTER_PU + + fun getOrganDonationRegisterInfoHost() = + BuildKonfig.ORGAN_DONATION_INFO } diff --git a/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt index 2572bc1e..2d75970d 100644 --- a/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt +++ b/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.ui diff --git a/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt index bc8ca8b1..c4d0c075 100644 --- a/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt +++ b/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt @@ -1,23 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("UnusedPrivateMember") - package de.gematik.ti.erp.app.utils.compose import androidx.compose.runtime.Composable @@ -33,9 +31,6 @@ fun OutlinedDebugButton( error("Debug button should only be used in debug builds!") } -fun Modifier.visualTestTag(tag: String) = - this - @Composable fun DebugOverlay(elements: Map) { error("Debug overlay should only be used in debug builds!") diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreenTest.kt new file mode 100644 index 00000000..469512d2 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/analytics/ui/OnboardingAllowAnalyticsScreenTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.analytics.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OnboardingAllowAnalyticsScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + AllowAnalyticsScreenScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreenTest.kt new file mode 100644 index 00000000..7e44ea43 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.appsecurity.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import org.junit.Test + +class InsecureDeviceScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + BooleanPreviewParameterProvider().values.forEach { checked -> + paparazzi.snapshot("checked_$checked") { + InsecureDeviceScreenPreview(checked = checked) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreenTest.kt new file mode 100644 index 00000000..20322783 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.appsecurity.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.BooleanPreviewParameterProvider +import org.junit.Test + +class IntegrityWarningScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + BooleanPreviewParameterProvider().values.forEach { checked -> + paparazzi.snapshot("checked_$checked") { + IntegrityWarningScreenPreview(checked) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreenTest.kt new file mode 100644 index 00000000..4655cd0f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockCanScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockCanScreenScaffoldPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.CanBooleanPreviewParameterProvider +import org.junit.Test + +class CardUnlockCanScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = CanBooleanPreviewParameterProvider().values.toList() + parameters.forEachIndexed { index, canNext -> + paparazzi.snapshot("parameter_$index") { + CardUnlockCanScreenScaffoldPreview(canNext = canNext) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreenTest.kt new file mode 100644 index 00000000..43501767 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockEgkScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockEgkScreenPreview +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreviewParameter +import de.gematik.ti.erp.app.screenshot.ReducedScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardUnlockEgkScreenTest(config: ScreenshotConfig) : ReducedScreenshotTest(config) { + @Test + fun screenShotTest() { + val unlockCardScreenPreviews = NfcPositionPreviewParameter().values.toList() + unlockCardScreenPreviews.forEachIndexed { index, unlockCardPreview -> + paparazzi.snapshot("parameter_$index ${unlockCardPreview.name}") { + CardUnlockEgkScreenPreview(previewData = unlockCardPreview) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreenTest.kt new file mode 100644 index 00000000..ed177693 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockIntroScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockIntroScreenScaffoldPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.UnlockMethodPreviewParameterProvider +import org.junit.Test + +class CardUnlockIntroScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = UnlockMethodPreviewParameterProvider().values.toList() + parameters.forEach { unlockMethod -> + paparazzi.snapshot("unlockMethod_$unlockMethod") { + CardUnlockIntroScreenScaffoldPreview(unlockMethod) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreenTest.kt new file mode 100644 index 00000000..bd614acb --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockNewSecretScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockNewSecretScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.CardUnlockNewSecretScreenPreviewDataProvider +import org.junit.Test + +class CardUnlockNewSecretScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = CardUnlockNewSecretScreenPreviewDataProvider().values.toList() + parameters.forEachIndexed { index, parameter -> + paparazzi.snapshot("parameter_$index") { + CardUnlockNewSecretScreenPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreenTest.kt new file mode 100644 index 00000000..2fd514bc --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockOldSecretScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockOldSecretScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.PinPreviewParameterProvider +import org.junit.Test + +class CardUnlockOldSecretScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = PinPreviewParameterProvider().values.toList() + parameters.forEachIndexed { _, pin -> + paparazzi.snapshot("parameter_$pin") { + CardUnlockOldSecretScreenPreview(pin) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreenTest.kt new file mode 100644 index 00000000..8c00c24f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardunlock/ui/CardUnlockPukScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardunlock.ui + +import de.gematik.ti.erp.app.cardunlock.ui.screens.CardUnlockPukScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.PukPreviewParameterProvider +import org.junit.Test + +class CardUnlockPukScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = PukPreviewParameterProvider().values.toList() + parameters.forEachIndexed { _, puk -> + paparazzi.snapshot("parameter_$puk") { + CardUnlockPukScreenPreview(puk) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreenTest.kt new file mode 100644 index 00000000..21fa8ea9 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallCanScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.preview.CanPreviewParameterProvider +import org.junit.Test + +class CardWallCanScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + CanPreviewParameterProvider().values.forEach { can -> + paparazzi.snapshot("can_$can") { + CardWallCanScreenPreview(can) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreenTest.kt new file mode 100644 index 00000000..76d71c4f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidHelpScreenTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallGidHelpScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + paparazzi.snapshot { + CardWallGidHelpScreenScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreenTest.kt new file mode 100644 index 00000000..40ad458f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallGidListScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.cardwall.ui.preview.HealthInsuranceDataPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallGidListScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = HealthInsuranceDataPreviewParameterProvider().values.toList() + parameters.forEachIndexed { index, healthInsuranceData -> + paparazzi.snapshot("$index") { + GidListScreenScaffoldPreview(healthInsuranceData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreenTest.kt new file mode 100644 index 00000000..0d9ec918 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallIntroScreenTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallIntroScreenContentPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallIntroScreenTest(val config: ScreenshotConfig) : BaseScreenshotTest(config = config) { + + @Test + fun screenShotTest() { + val params = CardWallIntroScreenContentPreviewParameterProvider().values.toList() + params.forEach { + paparazzi.snapshot(it.name) { + CardWallIntroScreenContentPreview(it) + } + } + } +} + +class CardWallIntroScreenAccessibilityTest(val config: ScreenshotConfig) : BaseAccessibilityTest(config = config) { + + @Test + fun screenShotTest() { + val params = CardWallIntroScreenContentPreviewParameterProvider().values.toList() + params.forEach { + paparazzi.accessibilitySnapshot(it.name) { + CardWallIntroScreenContentPreview(it) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreenTest.kt new file mode 100644 index 00000000..91369e26 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallPinScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallPinScreenPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallPinScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + CardWallPinScreenPreviewParameterProvider().values.forEach { previewData -> + paparazzi.snapshot(previewData.name) { + CardWallPinScreenPreview(previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreenTest.kt new file mode 100644 index 00000000..88096032 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallReadCardScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.cardwall.ui.preview.NfcPositionPreviewParameter +import de.gematik.ti.erp.app.screenshot.ReducedScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallReadCardScreenTest(config: ScreenshotConfig) : ReducedScreenshotTest(config) { + @Test + fun screenShotTest() { + val readCardScreenPreviews = NfcPositionPreviewParameter().values.toList() + readCardScreenPreviews.forEachIndexed { index, readCardPreview -> + paparazzi.snapshot("parameter_$index ${readCardPreview.name}") { + CardWallReadCardScreenPreview(previewData = readCardPreview) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreenTest.kt new file mode 100644 index 00000000..19946a85 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/cardwall/ui/screens/CardWallSaveCredentialsScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.cardwall.ui.screens + +import de.gematik.ti.erp.app.cardwall.ui.preview.CardWallSaveCredentialsPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class CardWallSaveCredentialsScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = CardWallSaveCredentialsPreviewParameterProvider().values.toList() + parameters.forEachIndexed { index, previewData -> + paparazzi.snapshot("parameter_$index ${previewData.name}") { + CardWallSaveCredentialsScreenPreview(previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenControllerTest.kt new file mode 100644 index 00000000..f1aacaa6 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/DosageInstructionBottomSheetScreenControllerTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction + +import de.gematik.ti.erp.app.medicationplan.usecase.GetDosageInstructionByTaskIdUseCase +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DosageInstructionBottomSheetScreenControllerTest { + private val prescriptionRepository: PrescriptionRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: DosageInstructionBottomSheetScreenController + private lateinit var getDosageInstructionByTaskIdUseCase: GetDosageInstructionByTaskIdUseCase + + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + getDosageInstructionByTaskIdUseCase = GetDosageInstructionByTaskIdUseCase(prescriptionRepository, dispatcher) + + controllerUnderTest = object : DosageInstructionBottomSheetScreenController( + getDosageInstructionByTaskIdUseCase = getDosageInstructionByTaskIdUseCase, + taskId = "taskId" + ) {} + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test getDosageInstruction empty`() { + coEvery { + prescriptionRepository + .loadSyncedTaskByTaskId(any()) + } returns flowOf() + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + runTest { + testScope.runTest { + advanceUntilIdle() + val dosageInstruction = controllerUnderTest.dosageInstruction.first() + assert(dosageInstruction.isErrorState) + } + } + } + + @Test + fun `test getDosageInstruction loading`() { + coEvery { + prescriptionRepository + .loadSyncedTaskByTaskId(any()) + } returns flowOf() + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + runTest { + testScope.runTest { + val dosageInstruction = controllerUnderTest.dosageInstruction.first() + assert(dosageInstruction.isLoading) + } + } + } + + @Test + fun `test getDosageInstruction empty data for scanned prescription`() { + coEvery { + prescriptionRepository + .loadSyncedTaskByTaskId(any()) + } returns flowOf() + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + runTest { + testScope.runTest { + advanceUntilIdle() + val dosageInstruction = controllerUnderTest.dosageInstruction.first() + assert(dosageInstruction.isDataState) + assert(dosageInstruction.data == MedicationPlanDosageInstruction.Empty) + } + } + } + + @Test + fun `test getDosageInstruction data for synced prescription`() { + coEvery { + prescriptionRepository + .loadScannedTaskByTaskId(any()) + } returns flowOf() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SYNCED_TASK) + runTest { + testScope.runTest { + advanceUntilIdle() + val dosageInstruction = controllerUnderTest.dosageInstruction.first() + assert(dosageInstruction.isDataState) + assert(dosageInstruction.data == MedicationPlanDosageInstruction.FreeText("Dosage")) + } + } + } + + @Test + fun `test structured getDosageInstruction data for synced prescription`() { + coEvery { + prescriptionRepository + .loadScannedTaskByTaskId(any()) + } returns flowOf() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns + flowOf(API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE) + runTest { + testScope.runTest { + advanceUntilIdle() + val dosageInstruction = controllerUnderTest.dosageInstruction.first() + assert(dosageInstruction.isDataState) + assert( + dosageInstruction.data == MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ) + ) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenControllerTest.kt new file mode 100644 index 00000000..003734e3 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationListScheduleScreenControllerTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import app.cash.turbine.test +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.model.toMedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.usecase.LoadProfilesWithSchedulesUseCase +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class MedicationListScheduleScreenControllerTest { + + private val profileRepository: ProfileRepository = mockk() + private val medicationPlanRepository: MedicationPlanRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var controllerUnderTest: MedicationListScheduleScreenController + private lateinit var loadProfilesWithSchedulesUseCase: LoadProfilesWithSchedulesUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + + loadProfilesWithSchedulesUseCase = LoadProfilesWithSchedulesUseCase( + medicationPlanRepository = medicationPlanRepository, + profileRepository = profileRepository, + dispatcher = dispatcher + ) + + controllerUnderTest = MedicationListScheduleScreenController( + loadProfilesWithSchedulesUseCase = loadProfilesWithSchedulesUseCase + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test loading state`() { + coEvery { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf(listOf()) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test empty state`() { + coEvery { profileRepository.profiles() } returns flowOf(listOf()) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf(listOf()) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val empty = awaitItem() + assertEquals(UiState.Empty(), empty) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test data state with active structured schedule`() { + val expectedSchedule = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE) + .toMedicationSchedule() + + coEvery { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf( + listOf( + expectedSchedule + ) + ) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val data = awaitItem() + assertEquals( + UiState.Data( + listOf( + ProfileWithSchedules( + profile = API_MOCK_PROFILE.toModel(), + medicationSchedules = listOf(expectedSchedule) + ) + ) + ), + data + ) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenControllerTest.kt new file mode 100644 index 00000000..e7c8e5d8 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationNotificationSuccessScreenControllerTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import app.cash.turbine.test +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.model.toMedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.usecase.LoadProfilesWithSchedulesUseCase +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.Instant +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class MedicationNotificationSuccessScreenControllerTest { + + private val now = Instant.parse("2024-01-01T12:00:00Z") + private val profileRepository: ProfileRepository = mockk() + private val medicationPlanRepository: MedicationPlanRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var controllerUnderTest: MedicationNotificationSuccessScreenController + private lateinit var loadProfilesWithSchedulesUseCase: LoadProfilesWithSchedulesUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + + loadProfilesWithSchedulesUseCase = LoadProfilesWithSchedulesUseCase( + medicationPlanRepository = medicationPlanRepository, + profileRepository = profileRepository, + dispatcher = dispatcher + ) + + controllerUnderTest = MedicationNotificationSuccessScreenController( + loadProfilesWithSchedulesUseCase = loadProfilesWithSchedulesUseCase, + now = now + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `test loading state`() { + coEvery { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf(listOf()) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test empty state`() { + coEvery { profileRepository.profiles() } returns flowOf(listOf()) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf(listOf()) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val empty = awaitItem() + assertEquals(UiState.Empty(), empty) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test data state with active structured schedule`() { + val schedule = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE, now = now) + .toMedicationSchedule() + val expectedSchedule = schedule.copy( + isActive = true + ) + + coEvery { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf( + listOf( + expectedSchedule + ) + ) + + testScope.runTest { + controllerUnderTest.profilesWithSchedules.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val data = awaitItem() + assertEquals( + UiState.Data( + listOf( + ProfileWithSchedules( + profile = API_MOCK_PROFILE.toModel(), + medicationSchedules = listOf(expectedSchedule) + ) + ) + ), + data + ) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenControllerTest.kt new file mode 100644 index 00000000..a298e97b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/presentation/MedicationPlanScheduleScreenControllerTest.kt @@ -0,0 +1,608 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.presentation + +import app.cash.turbine.test +import de.gematik.ti.erp.app.medicationplan.model.DateEvent +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.model.toMedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.usecase.GetDosageInstructionByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.LoadMedicationScheduleByTaskIdUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.PlanMedicationScheduleUseCase +import de.gematik.ti.erp.app.medicationplan.usecase.ScheduleReminderWorker +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.utils.maxLocalDate +import de.gematik.ti.erp.app.utils.toLocalDate +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.days + +class MedicationPlanScheduleScreenControllerTest { + + private val now = Instant.parse("2024-01-01T12:00:00Z") + private val profileRepository: ProfileRepository = mockk() + private val medicationPlanRepository: MedicationPlanRepository = mockk() + private val prescriptionRepository: PrescriptionRepository = mockk() + private val getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase = mockk() + private val scheduleReminderWorker: ScheduleReminderWorker = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var controllerUnderTest: MedicationPlanScheduleScreenController + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var getDosageInstructionByTaskIdUseCase: GetDosageInstructionByTaskIdUseCase + private lateinit var loadMedicationScheduleByTaskIdUseCase: LoadMedicationScheduleByTaskIdUseCase + private lateinit var planMedicationScheduleUseCase: PlanMedicationScheduleUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + every { scheduleReminderWorker.schedule() } returns Unit + Dispatchers.setMain(dispatcher) + getDosageInstructionByTaskIdUseCase = GetDosageInstructionByTaskIdUseCase(prescriptionRepository, dispatcher) + getActiveProfileUseCase = GetActiveProfileUseCase(profileRepository, dispatcher) + planMedicationScheduleUseCase = spyk( + PlanMedicationScheduleUseCase(scheduleReminderWorker, medicationPlanRepository, dispatcher) + ) + loadMedicationScheduleByTaskIdUseCase = LoadMedicationScheduleByTaskIdUseCase( + medicationPlanRepository, + dispatcher + ) + + controllerUnderTest = object : MedicationPlanScheduleScreenController( + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase, + getDosageInstructionByTaskIdUseCase = getDosageInstructionByTaskIdUseCase, + planMedicationScheduleUseCase = planMedicationScheduleUseCase, + loadMedicationScheduleByTaskIdUseCase = loadMedicationScheduleByTaskIdUseCase, + taskId = "taskId", + now = now + ) {} + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test initial state with scanned prescription`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK)) + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val data = awaitItem() + assertEquals( + UiState.Data( + PrescriptionSchedule( + prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK), + dosageInstruction = MedicationPlanDosageInstruction.Empty, + medicationSchedule = PrescriptionData.Scanned( + API_ACTIVE_SCANNED_TASK + ).toMedicationSchedule(now) + ) + ), + data + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test initial state with synced prescription`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK, now = now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + val expectedMedicationSchedule = expectedPrescription.toMedicationSchedule(now) + val expectedDosageInstruction = MedicationPlanDosageInstruction.FreeText("Dosage") + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val data = awaitItem() + assertEquals( + UiState.Data( + PrescriptionSchedule( + prescription = expectedPrescription, + dosageInstruction = expectedDosageInstruction, + medicationSchedule = expectedMedicationSchedule + ) + ), + data + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test initial state with synced prescription and medication schedule`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK, now = now) + val medicationSchedule = expectedPrescription.toMedicationSchedule(now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(medicationSchedule) + + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + val expectedDosageInstruction = MedicationPlanDosageInstruction.FreeText("Dosage") + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + val loading = awaitItem() + assertEquals(UiState.Loading(), loading) + advanceUntilIdle() + val data = awaitItem() + assertEquals( + UiState.Data( + PrescriptionSchedule( + prescription = expectedPrescription, + dosageInstruction = expectedDosageInstruction, + medicationSchedule = medicationSchedule + ) + ), + data + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test initial state with empty prescription should be error state`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns emptyFlow() + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + advanceUntilIdle() + val state = awaitItem() + assert(state.isErrorState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test add new time slot with scanned`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK, now = now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + val uuid = UUID.randomUUID().toString() + val expectedNotification = MedicationNotification( + id = uuid, + dosage = MedicationDosage("", ""), + time = LocalTime(12, 0) + ) + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + awaitItem() + controllerUnderTest.addNewTimeSlot( + dosage = MedicationDosage("", ""), + time = LocalTime(12, 0), + uuid = uuid + ) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals( + expectedNotification, + medicationSchedule.notifications.last() // Check the last added notification + ) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `remove notification`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE, now = now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + val notifications = item.data!!.medicationSchedule.notifications + assertEquals(2, notifications.size) + val notificationToDelete = notifications[0] + controllerUnderTest.removeNotification(notificationToDelete) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assert(medicationSchedule.notifications.size == 1) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test modify notification time`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE, now = now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + val newTime = LocalTime(14, 0) + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + val notifications = item.data!!.medicationSchedule.notifications + assertEquals(2, notifications.size) + val notificationToModify = notifications[0] + assertEquals(LocalTime(8, 0), notificationToModify.time) + controllerUnderTest.modifyNotificationTime(notification = notificationToModify, time = newTime) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(newTime, medicationSchedule.notifications[0].time) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test modify notification dosage`() { + val expectedPrescription = PrescriptionData.Synced(task = API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE, now = now) + + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(expectedPrescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns flowOf(expectedPrescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + val newMediationDosage = MedicationDosage("Tabletten", "2") + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + val notifications = item.data!!.medicationSchedule.notifications + assertEquals(2, notifications.size) + val notificationToModify = notifications[0] + assertEquals(MedicationDosage("TAB", "1"), notificationToModify.dosage) + controllerUnderTest.modifyDosage(notification = notificationToModify, dosage = newMediationDosage) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(newMediationDosage, medicationSchedule.notifications[0].dosage) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test activate schedule`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK)) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(false, item.data!!.medicationSchedule.isActive) + controllerUnderTest.activateSchedule() + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assert(medicationSchedule.isActive) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test deActivate schedule`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + val prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK) + val activeMedicationSchedule = prescription.toMedicationSchedule().copy(isActive = true) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(activeMedicationSchedule) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(true, item.data!!.medicationSchedule.isActive) + controllerUnderTest.deactivateSchedule() + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assert(!medicationSchedule.isActive) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test change scheduled date with date event start date`() { + val prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(prescription.task) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + val newDate = LocalDate(2024, 10, 22) + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(now.toLocalDate(), item.data!!.medicationSchedule.start) + controllerUnderTest.changeScheduledDate(DateEvent.StartDate(newDate)) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(newDate, medicationSchedule.start) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test change scheduled date with date event end date`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + val prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + val newDate = LocalDate(2025, 10, 22) + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + // default endDate of scanned is 10 days after + assertEquals(now.plus(10.days).toLocalDate(), item.data!!.medicationSchedule.end) + controllerUnderTest.changeScheduledDate(DateEvent.EndDate(newDate)) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(newDate, medicationSchedule.end) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test save endless date range`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + val prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + val currentDate = now.toLocalDate() + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(currentDate, item.data!!.medicationSchedule.start) + assertEquals(now.plus(10.days).toLocalDate(), item.data!!.medicationSchedule.end) + controllerUnderTest.saveEndlessDateRange(currentDate) + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(currentDate, medicationSchedule.start) + assertEquals(maxLocalDate(), medicationSchedule.end) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test calculate individual date range for scanned prescription`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + val prescription = PrescriptionData.Scanned(API_ACTIVE_SCANNED_TASK) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + val currentDate = now.toLocalDate() + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(currentDate, item.data!!.medicationSchedule.start) + assertEquals(now.plus(10.days).toLocalDate(), item.data!!.medicationSchedule.end) + controllerUnderTest.calculateIndividualDateRange() + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(currentDate, medicationSchedule.start) + assertEquals(currentDate, medicationSchedule.end) + } + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test calculate individual date range for synced pieceable prescription with structured dosage and amount`() { + val prescription = PrescriptionData.Synced(API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE) + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns + flowOf(prescription.task) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns + flowOf(prescription.toMedicationSchedule(now)) + coEvery { getPrescriptionByTaskIdUseCase(any()) } returns + flowOf(prescription) + coEvery { medicationPlanRepository.updateMedicationSchedule(any()) } returns Unit + + val currentDate = now.toLocalDate() + + testScope.runTest { + controllerUnderTest.prescriptionSchedule.test { + awaitItem() + advanceUntilIdle() + val item = awaitItem() + assert(item.isDataState) + assertEquals(currentDate, item.data!!.medicationSchedule.start) + assertEquals(now.plus(5.days).toLocalDate(), item.data!!.medicationSchedule.end) + controllerUnderTest.calculateIndividualDateRange() + } + advanceUntilIdle() + coVerify { + planMedicationScheduleUseCase( + withArg { medicationSchedule -> + assertEquals(currentDate, medicationSchedule.start) + assertEquals(now.plus(5.days).toLocalDate(), medicationSchedule.end) + } + ) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreenTest.kt new file mode 100644 index 00000000..168eee5d --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationListScheduleScreenTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import com.android.ide.common.rendering.api.SessionParams +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationListScheduleScreenPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MedicationListScheduleScreenTest(config: ScreenshotConfig) : BaseScreenshotTest( + config = config, + renderingMode = SessionParams.RenderingMode.SHRINK +) { + @Test + fun screenShotTest() { + val medicationListScheduleScreenPreviews = MedicationListScheduleScreenPreviewParameter().values.toList() + medicationListScheduleScreenPreviews.forEach { parameter -> + paparazzi.snapshot(parameter.name) { + MedicationListScheduleScreenPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreenTest.kt new file mode 100644 index 00000000..bf7c348b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationNotificationSuccessScreenTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import com.android.ide.common.rendering.api.SessionParams +import de.gematik.ti.erp.app.medicationplan.ui.components.MedicationNotificationSuccessScreenPreview +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationSuccessScreenPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MedicationNotificationSuccessScreenTest(config: ScreenshotConfig) : BaseScreenshotTest( + config = config, + renderingMode = SessionParams.RenderingMode.SHRINK +) { + @Test + fun screenShotTest() { + val medicationNotificationSuccessScreenPreviews = MedicationSuccessScreenPreviewParameter().values.toList() + medicationNotificationSuccessScreenPreviews.forEach { parameter -> + paparazzi.snapshot(parameter.name) { + MedicationNotificationSuccessScreenPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreenTest.kt new file mode 100644 index 00000000..5ea06ca9 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanDosageInfoBottomSheetScreenTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import com.android.ide.common.rendering.api.SessionParams +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationPlanDosageInfoPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MedicationPlanDosageInfoBottomSheetScreenTest(config: ScreenshotConfig) : BaseScreenshotTest( + config = config, + renderingMode = SessionParams.RenderingMode.SHRINK +) { + @Test + fun screenShotTest() { + val dosageInstructionBottomSheetPreviews = MedicationPlanDosageInfoPreviewParameter().values.toList() + dosageInstructionBottomSheetPreviews.forEach { dosageInstruction -> + paparazzi.snapshot(dosageInstruction.name) { + MedicationPlanDosageInfoContentPreview(dosageInstruction) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreenTest.kt new file mode 100644 index 00000000..f9c029b4 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/MedicationPlanScheduleScreenTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import com.android.ide.common.rendering.api.SessionParams +import de.gematik.ti.erp.app.medicationplan.ui.preview.MedicationPlanScheduleScreenPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MedicationPlanScheduleScreenTest(config: ScreenshotConfig) : BaseScreenshotTest( + config = config, + renderingMode = SessionParams.RenderingMode.SHRINK +) { + @Test + fun screenShotTest() { + val medicationPlanScheduleScreenPreviews = MedicationPlanScheduleScreenPreviewParameter().values.toList() + medicationPlanScheduleScreenPreviews.forEach { parameter -> + paparazzi.snapshot(parameter.name) { + MedicationPlanScheduleScreenPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreenTest.kt new file mode 100644 index 00000000..6166a13d --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/medicationplan/ui/ScheduleDateRangeScreenTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.ui + +import com.android.ide.common.rendering.api.SessionParams +import de.gematik.ti.erp.app.medicationplan.ui.preview.ScheduleDateRangeScreenPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class ScheduleDateRangeScreenTest(config: ScreenshotConfig) : BaseScreenshotTest( + config = config, + renderingMode = SessionParams.RenderingMode.SHRINK +) { + @Test + fun screenShotTest() { + val scheduleDateRangeScreenPreviews = ScheduleDateRangeScreenPreviewParameter().values.toList() + scheduleDateRangeScreenPreviews.forEach { parameter -> + paparazzi.snapshot(parameter.name) { + ScheduleDateRangeScreenPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/components/MessageSheetContentScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/components/MessageSheetContentScreenshotTest.kt new file mode 100644 index 00000000..5537cf0f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/components/MessageSheetContentScreenshotTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.ui.components + +import de.gematik.ti.erp.app.messages.presentation.ui.components.MessageBottomSheetScreenContentPreview +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessagesPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseComponentScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MessageSheetContentScreenshotTest(config: ScreenshotConfig) : BaseComponentScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = MessagesPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, message -> + paparazzi.snapshot("parameter_$index") { + MessageBottomSheetScreenContentPreview(message = message) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/screens/MessageListScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/screens/MessageListScreenTest.kt new file mode 100644 index 00000000..06ea7ed2 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/messages/ui/screens/MessageListScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.ui.screens + +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageListParameterProvider +import de.gematik.ti.erp.app.messages.presentation.ui.screens.MessageScreenContentPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MessageListScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = MessageListParameterProvider().values.toList() + parameters.forEachIndexed { index, orderData -> + paparazzi.snapshot("$index") { + MessageScreenContentPreview(orderData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreenTest.kt new file mode 100644 index 00000000..aac14e38 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitInformationScreenTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mlkit.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MlKitInformationScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + MlKitInformationScreenScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreenKtTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreenKtTest.kt new file mode 100644 index 00000000..633bc14b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mlkit/ui/MlKitScreenKtTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mlkit.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MlKitScreenKtTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + MlKitScreenScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/CommonMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/CommonMocks.kt new file mode 100644 index 00000000..859f88af --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/CommonMocks.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks + +import kotlinx.datetime.Instant + +const val PROFILE_ID = "profile-id-1" +const val TASK_ID = "task-id-1" +val DATE_2024_01_01 = Instant.parse("2024-01-01T10:00:00Z") +val DATE_3024_01_01 = Instant.parse("3024-01-01T10:00:00Z") +val DATE_3023_12_31 = Instant.parse("3023-12-31T10:00:00Z") diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/invoice/model/InvoiceMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/invoice/model/InvoiceMocks.kt new file mode 100644 index 00000000..093ed009 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/invoice/model/InvoiceMocks.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.invoice.model + +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.mocks.TASK_ID +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Instant +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +val MOCK_CHARGEABLE_ITEM = InvoiceData.ChargeableItem( + description = InvoiceData.ChargeableItem.Description.PZN("pzn"), + text = "text", + factor = 1.0, + price = InvoiceData.PriceComponent( + value = 10.0, + tax = 10.0 + ) +) + +val MOCK_INVOICE = InvoiceData.Invoice( + totalAdditionalFee = 10.0, + totalBruttoAmount = 10.0, + currency = "EUR", + chargeableItems = listOf(MOCK_CHARGEABLE_ITEM), + additionalDispenseItems = listOf(MOCK_CHARGEABLE_ITEM), + additionalInformation = listOf("additionalInformation") +) + +val MOCK_MEDICATION_REQUEST = SyncedTaskData.MedicationRequest( + null, null, null, SyncedTaskData.AccidentType.None, + null, null, false, null, + SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None +) + +val MOCK_INVOICE_ADDRESS = SyncedTaskData.Address( + line1 = "line1", + line2 = "line2", + postalCode = "postalCode", + city = "city" +) + +fun mockPkvInvoiceRecord( + profileId: String = PROFILE_ID, + taskId: String = TASK_ID +) = InvoiceData.PKVInvoiceRecord( + profileId = profileId, + taskId = taskId, + accessCode = "accessCode", + timestamp = DATE_2024_01_01, + pharmacyOrganization = Organization( + name = "pharmacyName", + address = MOCK_INVOICE_ADDRESS, + uniqueIdentifier = "uniqueIdentifier", + phone = "phone", + mail = "mail" + ), + practitionerOrganization = Organization( + name = "pharmacyName", + address = MOCK_INVOICE_ADDRESS, + uniqueIdentifier = "uniqueIdentifier", + phone = "phone", + mail = "mail" + ), + practitioner = SyncedTaskData.Practitioner( + name = "name", + qualification = "qualification", + practitionerIdentifier = "practitionerIdentifier" + ), + patient = SyncedTaskData.Patient( + name = "patientName", + address = MOCK_INVOICE_ADDRESS, + birthdate = null, + insuranceIdentifier = "insuranceIdentifier" + ), + medicationRequest = MOCK_MEDICATION_REQUEST, + whenHandedOver = FhirTemporal.Instant(Instant.DISTANT_PAST), + invoice = MOCK_INVOICE, + consumed = true +) + +fun mockedInvoiceChargeItemBundle(taskIds: List): JsonElement { + val entries = taskIds.map { taskId -> + JsonObject( + mapOf( + "fullUrl" to JsonPrimitive("http://hapi.fhir.org/baseR4/ChargeItem/$taskId"), + "resource" to JsonObject( + mapOf( + "resourceType" to JsonPrimitive("ChargeItem"), + "id" to JsonPrimitive(taskId), + "meta" to JsonObject( + mapOf( + "profile" to JsonArray( + listOf( + JsonPrimitive("https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_ChargeItem|1.0") + ) + ) + ) + ), + "status" to JsonPrimitive("billable"), + "extension" to JsonArray( + listOf( + JsonObject( + mapOf( + "url" to JsonPrimitive("https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_EX_MarkingFlag"), + "extension" to JsonArray( + listOf( + JsonObject( + mapOf("url" to JsonPrimitive("insuranceProvider"), "valueBoolean" to JsonPrimitive(false)) + ), + JsonObject( + mapOf("url" to JsonPrimitive("subsidy"), "valueBoolean" to JsonPrimitive(false)) + ), + JsonObject( + mapOf("url" to JsonPrimitive("taxOffice"), "valueBoolean" to JsonPrimitive(false)) + ) + ) + ) + ) + ) + ) + ), + "enterer" to JsonObject( + mapOf( + "identifier" to JsonObject( + mapOf( + "system" to JsonPrimitive("https://gematik.de/fhir/sid/telematik-id"), + "value" to JsonPrimitive("3-15.2.1456789123.191") + ) + ) + ) + ), + "identifier" to JsonArray( + listOf( + JsonObject( + mapOf( + "system" to JsonPrimitive("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId"), + "value" to JsonPrimitive(taskId) + ) + ), + JsonObject( + mapOf( + "system" to JsonPrimitive("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode"), + "value" to JsonPrimitive("access-code-placeholder") + ) + ) + ) + ), + "code" to JsonObject( + mapOf( + "coding" to JsonArray( + listOf( + JsonObject( + mapOf( + "code" to JsonPrimitive("not-applicable"), + "system" to JsonPrimitive("http://terminology.hl7.org/CodeSystem/data-absent-reason") + ) + ) + ) + ) + ) + ), + "subject" to JsonObject( + mapOf( + "identifier" to JsonObject( + mapOf( + "system" to JsonPrimitive("http://fhir.de/sid/gkv/kvid-10"), + "value" to JsonPrimitive("X234567890"), + "assigner" to JsonObject( + mapOf("display" to JsonPrimitive("Name einer privaten Krankenversicherung")) + ) + ) + ) + ) + ), + "enteredDate" to JsonPrimitive("2021-06-01T07:13:00+05:00"), + "supportingInformation" to JsonArray( + listOf( + JsonObject( + mapOf( + "reference" to JsonPrimitive("Bundle/0428d416-149e-48a4-977c-394887b3d85c"), + "display" to JsonPrimitive("https://fhir.kbv.de/StructureDefinition/KBV_PR_ERP_Bundle") + ) + ), + JsonObject( + mapOf( + "reference" to JsonPrimitive("Bundle/72bd741c-7ad8-41d8-97c3-9aabbdd0f5b4"), + "display" to JsonPrimitive( + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-AbgabedatenBundle" + ) + ) + ), + JsonObject( + mapOf( + "reference" to JsonPrimitive("Bundle/2fbc0103-1d1b-4be6-8ed8-6faf87bcc09b"), + "display" to JsonPrimitive("https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Bundle") + ) + ) + ) + ) + ) + ), + "search" to JsonObject( + mapOf("mode" to JsonPrimitive("match")) + ) + ) + ) + } + + return JsonObject( + mapOf( + "resourceType" to JsonPrimitive("Bundle"), + "id" to JsonPrimitive("200e3c55-b154-4335-a0ec-65addd39a3b6"), + "meta" to JsonObject( + mapOf("lastUpdated" to JsonPrimitive("2021-09-02T11:38:42.557+00:00")) + ), + "type" to JsonPrimitive("searchset"), + "total" to JsonPrimitive(taskIds.size), + "entry" to JsonArray(entries) + ) + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/order/model/OrderMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/order/model/OrderMocks.kt new file mode 100644 index 00000000..03925ab1 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/order/model/OrderMocks.kt @@ -0,0 +1,236 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.order.model + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.messages.repository.CachedPharmacy +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.datetime.Instant + +const val COMMUNICATION_ID = "communicationId1" +const val ORDER_ID = "orderId1" +const val TASK_ID = "testId1" +const val PHARMACY_NAME = "recipient" +const val PHARMACY_ID = "pharmacyId" +const val TELEMATIK_ID = "123" +const val WELCOME_MESSAGE_FROM = "E-Rezept App Team" +const val WELCOME_MESSAGE_TEXT = "Herzlich Willkommen in der E-Rezept App! Mit dieser App können Sie digital " + + "E-Rezepte empfangen und an eine Apotheke Ihrer Wahl senden." +const val IN_APP_MESSAGE_TEXT = "This is a long message to see how it looks like when the message is long and how the UI should handle it properly" +const val WELCOME_MESSAGE_TAG = "Herzlich Willkommen!" +const val WELCOME_MESSAGE_VERSION = "1.27.0" +const val WELCOME_MESSAGE_ID = "1" +const val WELCOME_MESSAGE_TIMESTAMP = "2024-01-01T10:00:00Z" +const val WELCOME_MESSAGE_GET_MESSAGE_TAG = "Neuerungen in der App Version 1.27.0" + +val CACHED_PHARMACY = CachedPharmacy(name = PHARMACY_NAME, telematikId = TELEMATIK_ID) + +val COMMUNICATION_DATA = Communication( + taskId = TASK_ID, + communicationId = COMMUNICATION_ID, + orderId = ORDER_ID, + profile = CommunicationProfile.ErxCommunicationDispReq, + sentOn = DATE_2024_01_01, + sender = "", + recipient = CachedPharmacy(name = PHARMACY_NAME, telematikId = TELEMATIK_ID).name, + payload = "", + consumed = true +) + +val MOCK_MESSAGE = OrderUseCaseData.Message( + communicationId = COMMUNICATION_ID, + sentOn = DATE_2024_01_01, + message = null, + pickUpCodeDMC = null, + pickUpCodeHR = null, + link = null, + consumed = true +) + +val ORDER = OrderUseCaseData.Order( + orderId = ORDER_ID, + prescriptions = listOf(null), // getting a list of null is possibly not the best approach + pharmacy = OrderUseCaseData.Pharmacy(PHARMACY_NAME, ""), + sentOn = DATE_2024_01_01, + hasUnreadMessages = true, + latestCommunicationMessage = null +) + +val inAppMessages = listOf( + InAppMessage( + id = "orderId1", + from = "", + text = IN_APP_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 1, + tag = "", + isUnread = true, + lastMessage = null, + messageProfile = null, + version = "" + ), + InAppMessage( + id = "orderId1", + from = "", + text = IN_APP_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 1, + tag = "", + isUnread = true, + lastMessage = null, + messageProfile = null, + version = "" + ), + InAppMessage( + id = WELCOME_MESSAGE_ID, + from = WELCOME_MESSAGE_FROM, + text = WELCOME_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 0, + tag = WELCOME_MESSAGE_TAG, + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = WELCOME_MESSAGE_VERSION + ) +) + +val inAppMessage = listOf( + InAppMessage( + id = "01", + from = "Team", + text = "This is a long message to see how it looks like when the message is long and how the UI should handle it properly", + timestamp = Instant.parse("2024-01-01T10:00:00Z"), + prescriptionsCount = 0, + tag = "1", + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = "1.27.1" + ), + InAppMessage( + id = WELCOME_MESSAGE_ID, + from = WELCOME_MESSAGE_FROM, + text = WELCOME_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 0, + tag = WELCOME_MESSAGE_TAG, + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = WELCOME_MESSAGE_VERSION + ) + +) + +val inAppMessagesMore = listOf( + InAppMessage( + id = "orderId1", + from = "", + text = IN_APP_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 1, + tag = "", + isUnread = true, + lastMessage = null, + messageProfile = null, + version = "" + ), + InAppMessage( + id = "orderId1", + from = "", + text = IN_APP_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 1, + tag = "", + isUnread = true, + lastMessage = null, + messageProfile = null, + version = "" + ), + InAppMessage( + id = "orderId1", + from = WELCOME_MESSAGE_FROM, + text = "This is a long message to see how it looks like when the message is long and how the UI should handle it properly", + timestamp = Instant.parse("2024-01-01T10:00:00Z"), + prescriptionsCount = 0, + tag = WELCOME_MESSAGE_GET_MESSAGE_TAG, + isUnread = false, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = "" + ) +) + +val welcomeMessage = + InAppMessage( + id = WELCOME_MESSAGE_ID, + from = WELCOME_MESSAGE_FROM, + text = WELCOME_MESSAGE_TEXT, + timestamp = Instant.parse(WELCOME_MESSAGE_TIMESTAMP), + prescriptionsCount = 0, + tag = WELCOME_MESSAGE_TAG, + isUnread = true, + lastMessage = null, + messageProfile = CommunicationProfile.InApp, + version = WELCOME_MESSAGE_VERSION + ) + +var internalEntity = listOf( + InAppMessageEntity().apply { + id = "01" + this.version = "1.27.1" + } +) + +val ORDER_DETAIL = OrderUseCaseData.OrderDetail( + orderId = ORDER_ID, + taskDetailedBundles = listOf( + OrderUseCaseData.TaskDetailedBundle( + invoiceInfo = OrderUseCaseData.InvoiceInfo( + hasInvoice = false, + invoiceSentOn = null + ), + prescription = null + ) + ), + sentOn = DATE_2024_01_01, + pharmacy = OrderUseCaseData.Pharmacy(PHARMACY_NAME, ""), + hasUnreadMessages = false +) + +val MOCK_PROFILE = ProfilesData.Profile( + id = "1", + name = "Mustermann", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.FemaleDoctor, + insuranceIdentifier = "12345567890", + insuranceType = ProfilesData.InsuranceType.GKV, + insurantName = "Mustermann", + insuranceName = "GesundheitsVersichert AG", + singleSignOnTokenScope = null, + active = false, + isConsentDrawerShown = false, + lastAuthenticated = null +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/pharmacy/model/PharmacyMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/pharmacy/model/PharmacyMocks.kt new file mode 100644 index 00000000..310db69a --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/pharmacy/model/PharmacyMocks.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.pharmacy.model + +import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.fhir.model.PharmacyAddress +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.mocks.order.model.PHARMACY_ID +import de.gematik.ti.erp.app.mocks.order.model.PHARMACY_NAME +import de.gematik.ti.erp.app.mocks.order.model.TELEMATIK_ID +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData + +val PHARMACY_DATA = PharmacyUseCaseData.Pharmacy( + id = PHARMACY_ID, + name = PHARMACY_NAME, + address = "\n ", + coordinates = null, + distance = null, + contacts = PharmacyContacts("", "", "", "", "", ""), + provides = emptyList(), + openingHours = null, + telematikId = TELEMATIK_ID +) + +val PHARMACY_DATA_FHIR = Pharmacy( + id = PHARMACY_ID, + name = PHARMACY_NAME, + address = PharmacyAddress(emptyList(), "", ""), + contacts = PharmacyContacts("", "", "", "", "", ""), + provides = emptyList(), + telematikId = TELEMATIK_ID +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/ScannedTaskDataMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/ScannedTaskDataMocks.kt new file mode 100644 index 00000000..cb6b9356 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/ScannedTaskDataMocks.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.api + +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData + +val API_ACTIVE_SCANNED_TASK = ScannedTaskData.ScannedTask( + profileId = PROFILE_ID, + taskId = "active-scanned-task-id-1", + index = 0, + name = "Scanned Task", + accessCode = "1234", + scannedOn = DATE_2024_01_01, + redeemedOn = null, + communications = emptyList() +) + +val API_ARCHIVE_SCANNED_TASK = ScannedTaskData.ScannedTask( + profileId = PROFILE_ID, + taskId = "archive-scanned-task-id-1", + index = 0, + name = "Scanned Task", + accessCode = "1234", + scannedOn = DATE_2024_01_01, + redeemedOn = DATE_2024_01_01, + communications = emptyList() +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/SyncedTaskDataMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/SyncedTaskDataMocks.kt new file mode 100644 index 00000000..6e20ac12 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/api/SyncedTaskDataMocks.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.api + +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.mocks.DATE_3024_01_01 +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Medication +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.utils.FhirTemporal +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.days + +private val ADDRESS = SyncedTaskData.Address( + line1 = "Hauptstraße 1", + line2 = "12345 Musterstadt", + postalCode = "12345", + city = "Musterstadt" +) + +internal val PATIENT = Patient( + name = "Erna Mustermann", + address = ADDRESS, + birthdate = null, + insuranceIdentifier = "AOK" +) + +private val MEDICATION = Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Medication", + form = "AEO", + lotNumber = "123456", + expirationDate = FhirTemporal.Instant(Clock.System.now().plus(30.days)), + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "KA", + amount = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ), + manufacturingInstructions = null, + packaging = null, + ingredients = emptyList(), + ingredientMedications = emptyList() +) + +private val MEDICATION_10_TAB = Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Medication", + form = "TAB", + lotNumber = "123456", + expirationDate = FhirTemporal.Instant(Clock.System.now().plus(30.days)), + identifier = SyncedTaskData.Identifier("1234567890"), + normSizeCode = "KA", + amount = Ratio( + numerator = Quantity( + value = "10", + unit = "TAB" + ), + denominator = null + ), + manufacturingInstructions = null, + packaging = null, + ingredients = emptyList(), + ingredientMedications = emptyList() +) + +internal var MEDICATION_REQUEST = MedicationRequest( + medication = MEDICATION, + dateOfAccident = null, + location = "Location", + emergencyFee = true, + dosageInstruction = "Dosage", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "Note", + substitutionAllowed = true +) + +internal var MEDICATION_REQUEST_DOSAGE_STRUCTURED_AMOUNT_20 = MedicationRequest( + medication = MEDICATION_10_TAB, + dateOfAccident = null, + location = "Location", + emergencyFee = true, + dosageInstruction = "1-0-1-0", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "Note", + substitutionAllowed = true, + quantity = 1 +) + +internal val PRACTITIONER = Practitioner( + name = "Dr. Max Mustermann", + qualification = "Arzt", + practitionerIdentifier = "1234567890" +) + +internal val INSURANCE_INFO = SyncedTaskData.InsuranceInformation( + name = "AOK", + status = "status", + coverageType = SyncedTaskData.CoverageType.GKV +) + +internal val ORGANIZATION = Organization( + name = "Praxis Dr. Mustermann", + address = ADDRESS, + uniqueIdentifier = "1234567890", + phone = "0123456789", + mail = "mustermann@praxis.de" +) + +val API_ACTIVE_SYNCED_TASK = SyncedTaskData.SyncedTask( + profileId = PROFILE_ID, + taskId = "active-synced-task-id-1", + accessCode = "1234", + lastModified = DATE_2024_01_01, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFO, + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_3024_01_01, + authoredOn = DATE_2024_01_01, + status = SyncedTaskData.TaskStatus.Ready, // to be active prescription + isIncomplete = false, + pvsIdentifier = "pvsIdentifier", + failureToReport = "failureToReport", + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = emptyList(), + lastMedicationDispense = null, + communications = emptyList() +) + +val API_ACTIVE_SYNCED_TASK_STRUCTURED_DOSAGE = SyncedTaskData.SyncedTask( + profileId = PROFILE_ID, + taskId = "active-synced-task-id-1", + accessCode = "1234", + lastModified = DATE_2024_01_01, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFO, + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_3024_01_01, + authoredOn = DATE_2024_01_01, + status = SyncedTaskData.TaskStatus.Ready, // to be active prescription + isIncomplete = false, + pvsIdentifier = "pvsIdentifier", + failureToReport = "failureToReport", + medicationRequest = MEDICATION_REQUEST_DOSAGE_STRUCTURED_AMOUNT_20, + medicationDispenses = emptyList(), + lastMedicationDispense = null, + communications = emptyList() +) + +val API_ARCHIVE_SYNCED_TASK = SyncedTaskData.SyncedTask( + profileId = PROFILE_ID, + taskId = "archive-synced-task-id-1", + accessCode = "1234", + lastModified = DATE_2024_01_01, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFO, + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_2024_01_01, + authoredOn = DATE_2024_01_01, + status = SyncedTaskData.TaskStatus.Completed, + isIncomplete = false, + pvsIdentifier = "pvsIdentifier", + failureToReport = "failureToReport", + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = emptyList(), + lastMedicationDispense = null, + communications = emptyList() +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/CommunicationMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/CommunicationMocks.kt new file mode 100644 index 00000000..51c02bef --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/CommunicationMocks.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.model + +import de.gematik.ti.erp.app.messages.domain.model.OrderUseCaseData +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import kotlinx.datetime.Instant + +private const val MOCK_COMMUNICATION_ID_01 = "CID-123-001" + +val COMMUNICATION_DATA = Communication( + taskId = "taskId1", + orderId = "", + communicationId = "communicationId1", + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = DATE_2024_01_01, + sender = "ABC123456", + recipient = "ABC654321", + payload = "payload1", + consumed = false +) + +val MOCK_MESSAGE_01 = OrderUseCaseData.Message( + communicationId = "MOCK_COMMUNICATION_ID_01", + sentOn = Instant.fromEpochSeconds(123456), + message = "mock message.", + pickUpCodeDMC = "Test_01___Rezept_01___abcdefg12345", + pickUpCodeHR = "T01__R01", + link = "https://www.tree.fm/forest/33", + consumed = false +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/PkvInvoiceMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/PkvInvoiceMocks.kt new file mode 100644 index 00000000..ca6fbe67 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/PkvInvoiceMocks.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.model + +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.asFhirTemporal + +val PKV_INVOICE_DATA = InvoiceData.PKVInvoiceRecord( + profileId = "1234", + taskId = "01234", + accessCode = "98765", + timestamp = DATE_2024_01_01, + invoice = InvoiceData.Invoice( + 2.30, + 6.80, + "EUR", + listOf(), + listOf() + ), + pharmacyOrganization = SyncedTaskData.Organization( + "Pharmacy", + SyncedTaskData.Address("", "", "", ""), + null, + null, + null + ), + practitionerOrganization = SyncedTaskData.Organization( + "Practitioner", + SyncedTaskData.Address("", "", "", ""), + null, + null, + null + ), + practitioner = SyncedTaskData.Practitioner("Practitioner", "", ""), + patient = SyncedTaskData.Patient( + "Patient", + SyncedTaskData.Address("", "", "", ""), + null, + null + ), + medicationRequest = SyncedTaskData.MedicationRequest( + null, null, null, SyncedTaskData.AccidentType.None, + null, null, false, null, + SyncedTaskData.MultiplePrescriptionInfo(false), 1, null, null, SyncedTaskData.AdditionalFee.None + ), + whenHandedOver = DATE_2024_01_01.asFhirTemporal(), + consumed = false +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/ScannedPrescriptionMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/ScannedPrescriptionMocks.kt new file mode 100644 index 00000000..654f7a14 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/ScannedPrescriptionMocks.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.model + +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription + +val MODEL_SCANNED_PRESCRIPTION_ACTIVE = Prescription.ScannedPrescription( + taskId = "active-scanned-task-id-1", + name = "Scanned Task", + redeemedOn = null, + scannedOn = DATE_2024_01_01, + index = 0, + communications = emptyList() +) + +val MODEL_SCANNED_PRESCRIPTION_ARCHIVED = Prescription.ScannedPrescription( + taskId = "archive-scanned-task-id-1", + name = "Scanned Task", + redeemedOn = DATE_2024_01_01, + scannedOn = DATE_2024_01_01, + index = 0, + communications = emptyList() +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/SyncedPrescriptionMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/SyncedPrescriptionMocks.kt new file mode 100644 index 00000000..bf1c756d --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/prescription/model/SyncedPrescriptionMocks.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.prescription.model + +import de.gematik.ti.erp.app.mocks.DATE_2024_01_01 +import de.gematik.ti.erp.app.mocks.DATE_3024_01_01 +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription + +val MODEL_SYNCED_PRESCRIPTION_ACTIVE = Prescription.SyncedPrescription( + taskId = "active-synced-task-id-1", + name = "Medication", + redeemedOn = null, + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_3024_01_01, + authoredOn = DATE_2024_01_01, + state = SyncedTaskData.SyncedTask.Ready( + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_3024_01_01 + ), + isIncomplete = false, + organization = "Dr. Max Mustermann", + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation() +) + +val MODEL_SYNCED_PRESCRIPTION_ARCHIVE = Prescription.SyncedPrescription( + taskId = "archive-synced-task-id-1", + name = "Medication", + redeemedOn = DATE_2024_01_01, + expiresOn = DATE_3024_01_01, + acceptUntil = DATE_2024_01_01, + authoredOn = DATE_2024_01_01, + state = SyncedTaskData.SyncedTask.Other( + state = SyncedTaskData.TaskStatus.Completed, + lastModified = DATE_2024_01_01 + ), + isIncomplete = false, + organization = "Dr. Max Mustermann", + isDirectAssignment = false, + prescriptionChipInformation = Prescription.PrescriptionChipInformation() +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/api/ProfileDataMock.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/api/ProfileDataMock.kt new file mode 100644 index 00000000..33a89c3b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/api/ProfileDataMock.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.profile.api + +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import io.mockk.mockk + +const val MOCK_SSO_TOKEN = "eyJlbmMiOiJBMjU2R0NNIiwiY3R5IjoiTkpXVCIsImV4cCI6MzMyNz" + + "U3MTcwMjEsImFsZyI6ImRpciIsImtpZCI6IjAwMDEifQ" + + "..X8msD7_H9N7lbgsl." + + "jOd-n06-fnH-oKygEzsp9rveKPR7x9qr2vqwgpoqFXUu63KUev14ASruJx9XHlxbmIkqe" + + "6TQ4GtPyEoKl5wb9CIPeIZ5qEkHKOEcYUS3MRl2BzdJQl32UKajKxDO_wg99sjpeab0QBy-" + + "L64DCNqeEbyGAM7NByRd9CwHysbcvfuF3Pw-vHYS9edN65ZHsWEfpbA8ZcvIRlouqyRVtt9o-" + + "avJXSgMiZbZkmh-qU71hxArXvLnqWuyHOT_fnlcnp7p_5TarKL1JDK70FKI3vT91Sabo_" + + "fzQdRXPJygiSKqn8-5fcDaJIyLySan0EzRT_mO8cwHDav7kjI5CwCsF4KcOEor6kvy-" + + "440SoiXZHsRKNMkRu-qrn2g7O5CFP5hpsBKo9j89PB_xvWgrYbYaSpxZBCBog58z8MKt" + + "ugU5HQ4rglIh2KJeBIDBFj7tqwkc7QJHPsmnI-Uo6I3kB39UkULxKuvgcVo5EeJdc-" + + "VhWyJyJEXFgDfV-ITSp-ZoYx2L4bIa4i2qWezWyB9ZK6bDv0tnqpta922meCo" + + "YQgjnFrYDgpF_4nP9aFqreCDcQPXNi2W2ndXeCWFk3-hSfRFvODLOVpyRN5Tpn9i" + + "73gZJuco_zGvQ2fy06lWP5HHzhNbG1luvEEVSjaKAe6WF5Upi647-Rr_GDNaumf" + + "6Uws7NRKMD20h5k_m745KjCUyp8mXZKgDJj0cHZxYByR085JvepZVWNia3HsXolK" + + "g1gj8C0LYqfJJNxaSysXj3-ERFp8P9UwpLFd9C9UYdzyDT2Oc5DitrZDLSAy1o2_" + + "Z-Nk640ihBa3Btv3y-02_HaKHaht5Uf4sBt0e_PPxYdBmc6UbhGbnRn1ULbZoaC_" + + "b0WveBbtw3eYxvYbxVHda3snDRVCqUbNa1SaytJE47fTft5p9g2e52i_" + + "Ougzt279jlEa15Ju-mcW-JuqUXhKtjZtWPtwlx6WcsVkIxPgCYd1ZM6" + + "kAY0U8MXvSu8EsMq-Z61XCDMBOhOQHwfA7-2vwEb7RRSi8Q4BzZnINI_" + + "s0dYH6xug5Uwve1CdMgzB2uSgivPKc9SyN5wqdjcfrpSzwdA.s1-YFy125OJvqElKe-qDCw" + +val API_MOCK_PROFILE = ProfilesData.Profile( + id = PROFILE_ID, + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = null, + name = "Erna Mustermann", + insurantName = "Erna Mustermann", + insuranceIdentifier = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + lastAuthenticated = mockk(), + lastTaskSynced = mockk(), + active = true, + singleSignOnTokenScope = null +) + +val API_MOCK_WITH_SSO_TOKEN_PROFILE = ProfilesData.Profile( + id = PROFILE_ID, + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = null, + name = "Erna Mustermann", + insurantName = "Erna Mustermann", + insuranceIdentifier = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + lastAuthenticated = mockk(), + lastTaskSynced = mockk(), + active = true, + singleSignOnTokenScope = IdpData.ExternalAuthenticationToken( + authenticatorId = "0001", + authenticatorName = "Authenticator", + token = IdpData.SingleSignOnToken(MOCK_SSO_TOKEN) + ) +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/model/ProfileMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/model/ProfileMocks.kt new file mode 100644 index 00000000..65f9ac8e --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/profile/model/ProfileMocks.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.profile.model + +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.datetime.Instant + +val MODEL_PROFILE = ProfilesUseCaseData.Profile( + id = "id", + name = "name", + insurance = ProfileInsuranceInformation( + insurantName = "insurantName", + insuranceIdentifier = "insuranceIdentifier", + insuranceName = "insuranceName", + insuranceType = ProfilesUseCaseData.InsuranceType.GKV + ), + isActive = true, + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = byteArrayOf(0x00, 0x01, 0x02), + lastAuthenticated = Instant.parse("2024-08-01T10:00:00Z"), + ssoTokenScope = null +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/settings/api/SettingsDataMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/settings/api/SettingsDataMocks.kt new file mode 100644 index 00000000..2f9187a3 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/mocks/settings/api/SettingsDataMocks.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.mocks.settings.api + +import de.gematik.ti.erp.app.settings.model.SettingsData + +val SETTINGS_DATA_GENERAL = SettingsData.General( + latestAppVersion = SettingsData.AppVersion( + code = 123, + name = "1.2.3" + ), + onboardingShownIn = null, + welcomeDrawerShown = false, + mainScreenTooltipsShown = false, + zoomEnabled = false, + userHasAcceptedInsecureDevice = false, + userHasAcceptedIntegrityNotOk = false, + mlKitAccepted = false, + trackingAllowed = false, + screenShotsAllowed = false +) diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreenTest.kt new file mode 100644 index 00000000..2dab40cb --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/BiometryScreenTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class BiometryScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + BiometryScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticScreenTest.kt new file mode 100644 index 00000000..b170bda3 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingAnalyticScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OnboardingAnalyticScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = listOf(true, false) + testParameters.forEach { boolean -> + paparazzi.snapshot("parameter_$boolean") { + OnboardingAnalyticsPreviewContentPreview(boolean) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreenTest.kt new file mode 100644 index 00000000..e97c9bbb --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingDataProtectionAndTermsOfUseOverviewScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OnboardingDataProtectionAndTermsOfUseOverviewScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = listOf(true, false) + testParameters.forEach { boolean -> + paparazzi.snapshot("parameter_$boolean") { + OnboardingDataProtectionAndTermsOfUseOverviewScreenContentPreview(boolean) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingPasswordAuthenticationScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingPasswordAuthenticationScreenTest.kt new file mode 100644 index 00000000..7a99a811 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingPasswordAuthenticationScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameterProvider +import org.junit.Test + +class OnboardingPasswordAuthenticationScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = SetAppPasswordParameterProvider().values.toList() + testParameters.forEach { parameter -> + paparazzi.snapshot(parameter.name) { + PasswordAuthenticationPreview(parameter) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreenTest.kt new file mode 100644 index 00000000..365f0660 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingWelcomeScreenTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OnboardingWelcomeScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + OnboardingWelcomeScreenContentPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/presentation/MessageDetailControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/presentation/MessageDetailControllerTest.kt new file mode 100644 index 00000000..240477c5 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/presentation/MessageDetailControllerTest.kt @@ -0,0 +1,501 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.order.messge.presentation + +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.fhir.model.PharmacyServices +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.messages.domain.repository.InAppLocalMessageRepository +import de.gematik.ti.erp.app.messages.domain.usecase.FetchInAppMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.FetchWelcomeMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessageUsingOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetProfileByOrderIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetRepliedMessagesUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.SetInternalMessageAsReadUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateCommunicationByOrderIdAndCommunicationIdUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.UpdateInvoicesByOrderIdAndTaskIdUseCase +import de.gematik.ti.erp.app.messages.presentation.MessageDetailController +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.mocks.order.model.COMMUNICATION_DATA +import de.gematik.ti.erp.app.mocks.order.model.COMMUNICATION_ID +import de.gematik.ti.erp.app.mocks.order.model.MOCK_MESSAGE +import de.gematik.ti.erp.app.mocks.order.model.MOCK_PROFILE +import de.gematik.ti.erp.app.mocks.order.model.ORDER_DETAIL +import de.gematik.ti.erp.app.mocks.order.model.ORDER_ID +import de.gematik.ti.erp.app.mocks.order.model.TASK_ID +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_GET_MESSAGE_TAG +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_ID +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TAG +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TEXT +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TIMESTAMP +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_VERSION +import de.gematik.ti.erp.app.mocks.order.model.inAppMessage +import de.gematik.ti.erp.app.mocks.order.model.internalEntity +import de.gematik.ti.erp.app.mocks.order.model.welcomeMessage +import de.gematik.ti.erp.app.mocks.pharmacy.model.PHARMACY_DATA +import de.gematik.ti.erp.app.mocks.pharmacy.model.PHARMACY_DATA_FHIR +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.GetPharmacyByTelematikIdUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertNotNull + +class MessageDetailControllerTest { + private val profileRepository: ProfileRepository = mockk() + private val communicationRepository: CommunicationRepository = mockk() + private val invoiceRepository: InvoiceRepository = mockk() + private val inAppMessageRepository: InAppMessageRepository = mockk() + private val messageResources: InAppMessageResources = mockk() + private val localMessageRepository: InAppLocalMessageRepository = mockk() + private val buildConfigInformation: BuildConfigInformation = mockk() + private val pharmacyRepository: PharmacyRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: MessageDetailController + private val clock = mockk() + + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var getRepliedMessagesUseCase: GetRepliedMessagesUseCase + private lateinit var getMessageUsingOrderIdUseCase: GetMessageUsingOrderIdUseCase + private lateinit var fetchInAppMessageUseCase: FetchInAppMessageUseCase + private lateinit var fetchWelcomeMessageUseCase: FetchWelcomeMessageUseCase + private lateinit var setInternalMessageIsReadUseCase: SetInternalMessageAsReadUseCase + private lateinit var updateCommunicationByCommunicationIdUseCase: UpdateCommunicationByCommunicationIdUseCase + private lateinit var updateCommunicationByOrderIdAndCommunicationIdUseCase: UpdateCommunicationByOrderIdAndCommunicationIdUseCase + private lateinit var updateInvoicesByOrderIdAndTaskIdUseCase: UpdateInvoicesByOrderIdAndTaskIdUseCase + private lateinit var getPharmacyByTelematikIdUseCase: GetPharmacyByTelematikIdUseCase + private lateinit var getProfileByOrderIdUseCase: GetProfileByOrderIdUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + + getActiveProfileUseCase = GetActiveProfileUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + setInternalMessageIsReadUseCase = SetInternalMessageAsReadUseCase( + inAppMessageRepository = inAppMessageRepository + + ) + fetchInAppMessageUseCase = FetchInAppMessageUseCase( + inAppMessageRepository = inAppMessageRepository, + localMessageRepository = localMessageRepository, + messageResources = messageResources + ) + getActiveProfileUseCase = GetActiveProfileUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + + getRepliedMessagesUseCase = GetRepliedMessagesUseCase( + communicationRepository = communicationRepository, + dispatcher = dispatcher + ) + getMessageUsingOrderIdUseCase = GetMessageUsingOrderIdUseCase( + communicationRepository = communicationRepository, + invoiceRepository = invoiceRepository, + dispatcher = dispatcher + ) + updateCommunicationByCommunicationIdUseCase = UpdateCommunicationByCommunicationIdUseCase( + repository = communicationRepository, + dispatcher = dispatcher + ) + updateCommunicationByOrderIdAndCommunicationIdUseCase = UpdateCommunicationByOrderIdAndCommunicationIdUseCase( + repository = communicationRepository, + dispatcher = dispatcher + ) + updateInvoicesByOrderIdAndTaskIdUseCase = UpdateInvoicesByOrderIdAndTaskIdUseCase( + communicationRepository = communicationRepository, + invoiceRepository = invoiceRepository, + dispatcher = dispatcher + ) + getPharmacyByTelematikIdUseCase = GetPharmacyByTelematikIdUseCase( + repository = pharmacyRepository, + dispatchers = dispatcher + ) + getProfileByOrderIdUseCase = GetProfileByOrderIdUseCase( + communicationRepository = communicationRepository, + dispatcher = dispatcher + ) + fetchWelcomeMessageUseCase = spyk(FetchWelcomeMessageUseCase(inAppMessageRepository, buildConfigInformation, messageResources)) + fetchInAppMessageUseCase = spyk(FetchInAppMessageUseCase(inAppMessageRepository, localMessageRepository, messageResources)) + controllerUnderTest = MessageDetailController( + orderId = ORDER_ID, + isLocalMessage = false, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageIsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + + every { clock.now() } returns Instant.parse(WELCOME_MESSAGE_TIMESTAMP) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when orderId is not provided`() { + coEvery { communicationRepository.profileByOrderId(ORDER_ID) } returns flowOf(MOCK_PROFILE) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + + val messagesResult = controllerUnderTest.messages.first() + val orderStateResult = controllerUnderTest.order.first() + val pharmacyStateResult = controllerUnderTest.pharmacy.first() + + assert(messagesResult.isLoadingState) + assert(orderStateResult.isErrorState) + assert(pharmacyStateResult.isEmptyState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when orderId is provided and returns data successfully`() { + val testMessages = listOf(MOCK_MESSAGE) + + every { getRepliedMessagesUseCase(ORDER_ID, "") } returns flowOf(testMessages) + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(ORDER_DETAIL) + coEvery { pharmacyRepository.searchPharmacyByTelematikId(any()) } returns Result.success( + PharmacyServices(listOf(PHARMACY_DATA_FHIR), "", 1) + ) + + every { communicationRepository.taskIdsByOrder(ORDER_ID) } returns flowOf(listOf(TASK_ID)) + every { communicationRepository.loadRepliedCommunications(listOf(TASK_ID), any()) } returns flowOf( + listOf(COMMUNICATION_DATA) + ) + every { communicationRepository.loadDispReqCommunications(ORDER_ID) } returns flowOf( + listOf( + COMMUNICATION_DATA + ) + ) + every { communicationRepository.loadPharmacies() } returns flowOf(emptyList()) + every { communicationRepository.hasUnreadDispenseMessage(any(), any()) } returns flowOf(false) + every { invoiceRepository.invoiceByTaskId(TASK_ID) } returns flowOf(null) + every { invoiceRepository.invoiceByTaskId(eq(TASK_ID)) } returns flowOf(null) + every { communicationRepository.loadSyncedByTaskId(TASK_ID) } returns flowOf(null) + every { communicationRepository.loadScannedByTaskId(TASK_ID) } returns flowOf(null) + coEvery { communicationRepository.downloadMissingPharmacy(any()) } returns Result.success(null) + coEvery { communicationRepository.profileByOrderId(ORDER_ID) } returns flowOf(MOCK_PROFILE) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + + val messagesResult = controllerUnderTest.messages.first() + val orderStateResult = controllerUnderTest.order.first() + val pharmacyStateResult = controllerUnderTest.pharmacy.first() + val actualMessages = messagesResult.data + + assertEquals(testMessages, actualMessages) + assert(orderStateResult.isDataState) + assertEquals(ORDER_DETAIL, orderStateResult.data) + assert(pharmacyStateResult.isDataState) + assertEquals(PHARMACY_DATA, pharmacyStateResult.data) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when orderId is provided but messages and order data are not found`() { + every { getRepliedMessagesUseCase(ORDER_ID, "") } returns flowOf(emptyList()) + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(null) + coEvery { pharmacyRepository.searchPharmacyByTelematikId(any()) } returns Result.success( + PharmacyServices(emptyList(), "", 0) + ) + + every { communicationRepository.taskIdsByOrder(ORDER_ID) } returns flowOf(emptyList()) + every { communicationRepository.loadRepliedCommunications(emptyList(), "") } returns flowOf(emptyList()) + every { communicationRepository.loadDispReqCommunications(ORDER_ID) } returns flowOf(emptyList()) + every { communicationRepository.loadPharmacies() } returns flowOf(emptyList()) + every { communicationRepository.hasUnreadDispenseMessage(any(), any()) } returns flowOf(false) + every { invoiceRepository.invoiceByTaskId(any()) } returns flowOf(null) + every { communicationRepository.loadSyncedByTaskId(any()) } returns flowOf(null) + every { communicationRepository.loadScannedByTaskId(any()) } returns flowOf(null) + coEvery { communicationRepository.downloadMissingPharmacy(any()) } returns Result.success(null) + coEvery { communicationRepository.setCommunicationStatus(any(), any()) } returns Unit + coEvery { communicationRepository.profileByOrderId(ORDER_ID) } returns flowOf(MOCK_PROFILE) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + + val messagesResult = controllerUnderTest.messages.first() + val orderStateResult = controllerUnderTest.order.first() + val pharmacyStateResult = controllerUnderTest.pharmacy.first() + + messagesResult.data?.let { assert(it.isEmpty()) } + assert(orderStateResult.isEmptyState) + assert(pharmacyStateResult.isLoadingState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when orderId is provided and use cases throw errors`() { + every { getRepliedMessagesUseCase(ORDER_ID, "") } throws IllegalArgumentException("Messages error") + every { getMessageUsingOrderIdUseCase(ORDER_ID) } throws IllegalArgumentException("Order error") + coEvery { + pharmacyRepository.searchPharmacyByTelematikId(any()) + } throws IllegalArgumentException("Pharmacy error") + coEvery { communicationRepository.profileByOrderId(ORDER_ID) } returns flowOf(MOCK_PROFILE) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + + val orderStateResult = controllerUnderTest.order.first() + val pharmacyStateResult = controllerUnderTest.pharmacy.first() + + assert(orderStateResult.isErrorState) + assert(pharmacyStateResult.isErrorState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `fetch inapp messages from local database`() { + val mockMessages = inAppMessage + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { fetchInAppMessageUseCase.invoke() } returns flowOf(mockMessages) + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(null) + coEvery { inAppMessageRepository.setInternalMessageAsRead() } returns Unit + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(internalEntity) + + controllerUnderTest = MessageDetailController( + orderId = ORDER_ID, + isLocalMessage = true, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageIsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + val messageList = controllerUnderTest._localMessages.value + + assertNotNull(messageList) + assertEquals(messageList, inAppMessage) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `fetch inapp messages from local database return empty list`() { + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(null) + coEvery { inAppMessageRepository.setInternalMessageAsRead() } returns Unit + coEvery { buildConfigInformation.versionName() } returns (WELCOME_MESSAGE_VERSION) + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { localMessageRepository.getInternalMessages() } returns flowOf(emptyList()) + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(emptyList()) + + controllerUnderTest = MessageDetailController( + orderId = ORDER_ID, + isLocalMessage = true, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageIsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + val messageList = controllerUnderTest._localMessages.value + assertNotNull(messageList) + kotlin.test.assertEquals(true, messageList.isEmpty()) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `fetch inapp messages from local database return empty list with welcome message`() { + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(null) + coEvery { fetchWelcomeMessageUseCase.invoke() } returns flowOf(welcomeMessage) + coEvery { inAppMessageRepository.setInternalMessageAsRead() } returns Unit + coEvery { buildConfigInformation.versionName() } returns (WELCOME_MESSAGE_VERSION) + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(true) + coEvery { localMessageRepository.getInternalMessages() } returns flowOf(emptyList()) + coEvery { messageResources.messageFrom } returns (WELCOME_MESSAGE_ID) + coEvery { messageResources.welcomeMessage } returns (WELCOME_MESSAGE_TEXT) + coEvery { messageResources.welcomeMessageTag } returns (WELCOME_MESSAGE_TAG) + coEvery { messageResources.getMessageTag(any()) } returns (WELCOME_MESSAGE_GET_MESSAGE_TAG) + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(emptyList()) + coEvery { fetchWelcomeMessageUseCase.getCurrentTimeAsString() } returns (WELCOME_MESSAGE_VERSION) + + controllerUnderTest = MessageDetailController( + orderId = ORDER_ID, + isLocalMessage = true, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageIsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + val messageList = controllerUnderTest._localMessages.value + assertNotNull(messageList) + assertEquals(messageList, listOf(welcomeMessage)) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `consumeAllMessages updates communication statuses correctly`() { + val mockMessages = listOf(MOCK_MESSAGE) + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + every { getRepliedMessagesUseCase(ORDER_ID, "") } returns flowOf(mockMessages) + every { getMessageUsingOrderIdUseCase(ORDER_ID) } returns flowOf(ORDER_DETAIL) + coEvery { pharmacyRepository.searchPharmacyByTelematikId(any()) } returns Result.success( + PharmacyServices(emptyList(), "", 0) + ) + + every { communicationRepository.taskIdsByOrder(ORDER_ID) } returns flowOf(listOf(TASK_ID)) + every { communicationRepository.loadRepliedCommunications(listOf(TASK_ID), any()) } returns flowOf( + listOf( + COMMUNICATION_DATA + ) + ) + every { communicationRepository.loadDispReqCommunications(ORDER_ID) } returns flowOf( + listOf( + COMMUNICATION_DATA + ) + ) + every { communicationRepository.loadPharmacies() } returns flowOf(emptyList()) + every { communicationRepository.hasUnreadDispenseMessage(any(), any()) } returns flowOf(false) + every { invoiceRepository.invoiceByTaskId(TASK_ID) } returns flowOf(null) + coEvery { invoiceRepository.updateInvoiceCommunicationStatus(TASK_ID, true) } just Runs + every { communicationRepository.loadSyncedByTaskId(TASK_ID) } returns flowOf(null) + every { communicationRepository.loadScannedByTaskId(TASK_ID) } returns flowOf(null) + coEvery { communicationRepository.downloadMissingPharmacy(any()) } returns Result.success(null) + coEvery { communicationRepository.setCommunicationStatus(any(), any()) } just Runs + coEvery { communicationRepository.profileByOrderId(ORDER_ID) } returns flowOf(MOCK_PROFILE) + + val updateCommunicationByOrderIdAndCommunicationIdUseCase = spyk( + UpdateCommunicationByOrderIdAndCommunicationIdUseCase(communicationRepository, dispatcher) + ) + val updateCommunicationByCommunicationIdUseCase = spyk( + UpdateCommunicationByCommunicationIdUseCase(communicationRepository, dispatcher) + ) + + controllerUnderTest = MessageDetailController( + orderId = ORDER_ID, + isLocalMessage = false, + getRepliedMessagesUseCase = getRepliedMessagesUseCase, + getMessageUsingOrderIdUseCase = getMessageUsingOrderIdUseCase, + fetchInAppMessageUseCase = fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase = fetchWelcomeMessageUseCase, + setInternalMessageIsReadUseCase = setInternalMessageIsReadUseCase, + updateCommunicationByCommunicationIdUseCase = updateCommunicationByCommunicationIdUseCase, + updateCommunicationByOrderIdAndCommunicationIdUseCase = updateCommunicationByOrderIdAndCommunicationIdUseCase, + updateInvoicesByOrderIdAndTaskIdUseCase = updateInvoicesByOrderIdAndTaskIdUseCase, + getPharmacyByTelematikIdUseCase = getPharmacyByTelematikIdUseCase, + getProfileByOrderIdUseCase = getProfileByOrderIdUseCase + ) + + testScope.runTest { + controllerUnderTest.init() + advanceUntilIdle() + + controllerUnderTest.consumeAllMessages {} + advanceUntilIdle() + + coVerify(exactly = 1) { updateCommunicationByOrderIdAndCommunicationIdUseCase(ORDER_ID) } + coVerify(exactly = 1) { updateCommunicationByCommunicationIdUseCase(COMMUNICATION_ID) } + } + } +} + +interface Clock { + fun now(): Instant +} + +class SystemClock : Clock { + override fun now(): Instant = Clock.System.now() +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/ui/MessageDetailScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/ui/MessageDetailScreenTest.kt new file mode 100644 index 00000000..b7383755 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messge/ui/MessageDetailScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.order.messge.ui + +import de.gematik.ti.erp.app.messages.presentation.ui.preview.MessageOrderDetailPreviewParameterProvider +import de.gematik.ti.erp.app.messages.presentation.ui.screens.MessageDetailScreenWithPharmacyPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class MessageDetailScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = MessageOrderDetailPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, message -> + paparazzi.snapshot("parameter_$index") { + MessageDetailScreenWithPharmacyPreview(message) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messges/presentation/MessageListControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messges/presentation/MessageListControllerTest.kt new file mode 100644 index 00000000..ad0505a7 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/order/messges/presentation/MessageListControllerTest.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.order.messges.presentation + +import android.content.Context +import de.gematik.ti.erp.app.changelogs.InAppMessageRepository +import de.gematik.ti.erp.app.featuretoggle.model.NewFeature +import de.gematik.ti.erp.app.featuretoggle.repository.NewFeaturesRepository +import de.gematik.ti.erp.app.featuretoggle.usecase.IsNewFeatureSeenUseCase +import de.gematik.ti.erp.app.featuretoggle.usecase.MarkNewFeatureSeenUseCase +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.domain.model.InAppMessage +import de.gematik.ti.erp.app.messages.domain.model.InAppMessageResources +import de.gematik.ti.erp.app.messages.domain.repository.InAppLocalMessageRepository +import de.gematik.ti.erp.app.messages.domain.usecase.FetchInAppMessageUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.GetMessagesUseCase +import de.gematik.ti.erp.app.messages.domain.usecase.FetchWelcomeMessageUseCase +import de.gematik.ti.erp.app.messages.presentation.MessageListController +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.mocks.order.model.CACHED_PHARMACY +import de.gematik.ti.erp.app.mocks.order.model.COMMUNICATION_DATA +import de.gematik.ti.erp.app.mocks.order.model.IN_APP_MESSAGE_TEXT +import de.gematik.ti.erp.app.mocks.order.model.TASK_ID +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_FROM +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_GET_MESSAGE_TAG +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TAG +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TEXT +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_TIMESTAMP +import de.gematik.ti.erp.app.mocks.order.model.WELCOME_MESSAGE_VERSION +import de.gematik.ti.erp.app.mocks.order.model.inAppMessages +import de.gematik.ti.erp.app.mocks.order.model.inAppMessagesMore +import de.gematik.ti.erp.app.mocks.order.model.internalEntity +import de.gematik.ti.erp.app.mocks.order.model.welcomeMessage +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class MessageListControllerTest { + private val communicationRepository: CommunicationRepository = mockk() + private val profileRepository: ProfileRepository = mockk() + private val invoiceRepository: InvoiceRepository = mockk() + private val inAppMessageRepository: InAppMessageRepository = mockk() + + private val messageResources: InAppMessageResources = mockk() + private val localMessageRepository: InAppLocalMessageRepository = mockk() + private val buildConfigInformation: BuildConfigInformation = mockk() + private val newFeatureRepository: NewFeaturesRepository = mockk() + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private val mockContext = mockk() + + private lateinit var controllerUnderTest: MessageListController + + private lateinit var getMessagesUseCase: GetMessagesUseCase + private lateinit var isNewFeatureSeenUseCase: IsNewFeatureSeenUseCase + private lateinit var markNewFeatureSeenUseCase: MarkNewFeatureSeenUseCase + private lateinit var getProfilesUseCase: GetProfilesUseCase + private lateinit var fetchInAppMessageUseCase: FetchInAppMessageUseCase + private lateinit var fetchWelcomeMessageUseCase: FetchWelcomeMessageUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + getMessagesUseCase = mockk() + getProfilesUseCase = mockk() + fetchInAppMessageUseCase = mockk() + isNewFeatureSeenUseCase = mockk() + markNewFeatureSeenUseCase = spyk(MarkNewFeatureSeenUseCase(newFeatureRepository, dispatcher)) + + coEvery { + profileRepository.profiles() + } returns flowOf(listOf(API_MOCK_PROFILE, API_MOCK_PROFILE.copy(id = "2", active = false))) + coEvery { communicationRepository.loadPharmacies() } returns flowOf(listOf(CACHED_PHARMACY)) + coEvery { communicationRepository.hasUnreadDispenseMessage(any(), any()) } returns flowOf(true) + coEvery { communicationRepository.hasUnreadRepliedMessages(any(), any()) } returns flowOf(true) + coEvery { communicationRepository.downloadMissingPharmacy(any()) } returns Result.success(null) + coEvery { communicationRepository.loadSyncedByTaskId(any()) } returns flowOf(null) + coEvery { communicationRepository.loadScannedByTaskId(any()) } returns flowOf(null) + coEvery { communicationRepository.taskIdsByOrder(any()) } returns flowOf(listOf(TASK_ID)) + coEvery { communicationRepository.loadFirstDispReqCommunications(any()) } returns flowOf(listOf(COMMUNICATION_DATA)) + coEvery { communicationRepository.loadRepliedCommunications(any(), any()) } returns flowOf(emptyList()) + coEvery { communicationRepository.loadDispReqCommunications(any()) } returns flowOf(emptyList()) + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(internalEntity) + coEvery { localMessageRepository.getInternalMessages() } returns flowOf(inAppMessages) + coEvery { newFeatureRepository.setDefaults() } returns Unit + coEvery { newFeatureRepository.isNewFeatureSeen(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) } returns false + coEvery { newFeatureRepository.markFeatureSeen(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) } returns Unit + coEvery { invoiceRepository.hasUnreadInvoiceMessages(any()) } returns flowOf(false) + every { mockContext.getString(any()) } returns IN_APP_MESSAGE_TEXT + + getMessagesUseCase = GetMessagesUseCase(communicationRepository, invoiceRepository, profileRepository, dispatcher) + getProfilesUseCase = GetProfilesUseCase(profileRepository, dispatcher) + isNewFeatureSeenUseCase = IsNewFeatureSeenUseCase(newFeatureRepository, dispatcher) + markNewFeatureSeenUseCase = spyk(MarkNewFeatureSeenUseCase(newFeatureRepository, dispatcher)) + fetchInAppMessageUseCase = spyk(FetchInAppMessageUseCase(inAppMessageRepository, localMessageRepository, messageResources)) + fetchWelcomeMessageUseCase = spyk(FetchWelcomeMessageUseCase(inAppMessageRepository, buildConfigInformation, messageResources)) + controllerUnderTest = + MessageListController( + getMessagesUseCase, + getProfilesUseCase, + fetchInAppMessageUseCase, + fetchWelcomeMessageUseCase, + isNewFeatureSeenUseCase, + markNewFeatureSeenUseCase, + mockContext + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `fetch orders from local database and check if the new feature was seen on init`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { communicationRepository.taskIdsByOrder(any()) } returns flowOf(listOf(TASK_ID)) + coEvery { messageResources.messageFrom } returns (WELCOME_MESSAGE_FROM) + coEvery { messageResources.welcomeMessage } returns (WELCOME_MESSAGE_TEXT) + coEvery { messageResources.welcomeMessageTag } returns (WELCOME_MESSAGE_TAG) + coEvery { messageResources.getMessageTag(any()) } returns (WELCOME_MESSAGE_GET_MESSAGE_TAG) + coEvery { buildConfigInformation.versionName() } returns (WELCOME_MESSAGE_VERSION) + coEvery { fetchWelcomeMessageUseCase.getCurrentTimeAsString() } returns (WELCOME_MESSAGE_TIMESTAMP) + + testScope.runTest { + advanceUntilIdle() + val orders = controllerUnderTest.viewState.first() + + val isOrderFeatureChangeSeen = controllerUnderTest.isMessagesListFeatureChangeSeen.first() + println("Feature change seen: $isOrderFeatureChangeSeen") + + // orders check + val data: List? = orders.messagesList.data + println("Orders state: $data") + assertEquals(inAppMessagesMore, data) + + // isOrderFeatureChangeSeen check + assertEquals(true, isOrderFeatureChangeSeen) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `do not show the feature change bar if only one profile is present`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + testScope.runTest { + advanceUntilIdle() + val isOrderFeatureChangeSeen = controllerUnderTest.isMessagesListFeatureChangeSeen.first() + + // if more than one profile is present, only then the actual value is used + assertEquals(true, isOrderFeatureChangeSeen) + } + } + + @Test + fun `start the controller on loading state`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + testScope.runTest { + val orders = controllerUnderTest.viewState.first() + // orders check + assert(orders.messagesList.isLoadingState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `show error state on exception`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { communicationRepository.loadScannedByTaskId(any()) } throws Exception("Test exception") + testScope.runTest { + advanceUntilIdle() + val orders = controllerUnderTest.viewState.first() + // orders check + assert(orders.messagesList.isErrorState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `show empty state on no communications`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(emptyList()) + coEvery { communicationRepository.loadFirstDispReqCommunications(any()) } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + val orders = controllerUnderTest.viewState.first() + // orders check + assertEquals(true, orders.messagesList.isEmptyState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `show empty state on no profiles`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { inAppMessageRepository.inAppMessages } returns flowOf(emptyList()) + coEvery { profileRepository.profiles() } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + val orders = controllerUnderTest.viewState.first() + // orders check + assertEquals(true, orders.messagesList.isEmptyState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `show data state on no task-ids`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + coEvery { communicationRepository.taskIdsByOrder(any()) } returns flowOf(emptyList()) + coEvery { fetchWelcomeMessageUseCase.invoke() } returns flowOf(welcomeMessage) + coEvery { fetchInAppMessageUseCase.invoke() } returns flowOf(inAppMessages) + testScope.runTest { + advanceUntilIdle() + val orders = controllerUnderTest.viewState.first() + // orders check + assertEquals(true, orders.messagesList.isDataState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `check that the repository call is made on user clicking on the new feature label`() { + coEvery { inAppMessageRepository.showWelcomeMessage } returns flowOf(false) + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.markProfileTopBarRemovedChangeSeen() + } + coVerify(exactly = 1) { markNewFeatureSeenUseCase.invoke(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) } + coVerify(exactly = 1) { newFeatureRepository.markFeatureSeen(NewFeature.ORDERS_SCREEN_NO_PROFILE_BAR) } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectInsuranceCompanyScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectInsuranceCompanyScreenshotTest.kt new file mode 100644 index 00000000..009337b0 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectInsuranceCompanyScreenshotTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui.screen + +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectInsuranceCompanyScreenPreview +import de.gematik.ti.erp.app.orderhealthcard.ui.preview.OrderHealthCardPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OrderHealthCardSelectInsuranceCompanyScreenshotTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = OrderHealthCardPreviewParameter().values.toList() + testParameters.forEachIndexed { index, value -> + paparazzi.snapshot("parameter_$index") { + OrderHealthCardSelectInsuranceCompanyScreenPreview(value) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectMethodScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectMethodScreenshotTest.kt new file mode 100644 index 00000000..b8f47204 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectMethodScreenshotTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui.screen + +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectMethodScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OrderHealthCardSelectMethodScreenshotTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + OrderHealthCardSelectMethodScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectOptionScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectOptionScreenshotTest.kt new file mode 100644 index 00000000..2201af60 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/screen/OrderHealthCardSelectOptionScreenshotTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.orderhealthcard.ui.screen + +import de.gematik.ti.erp.app.orderhealthcard.ui.OrderHealthCardSelectOptionScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OrderHealthCardSelectOptionScreenshotTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + OrderHealthCardSelectOptionScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromMessageScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromMessageScreenTest.kt new file mode 100644 index 00000000..b8a817ca --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromMessageScreenTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PharmacyDetailsFromMessageScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + PharmacyDetailsScreenFromMessagePreview() + } + } +} + +class PharmacyDetailsFromMessageScreenAccessibilityTest(config: ScreenshotConfig) : BaseAccessibilityTest(config) { + + @Test + fun screenShotTest() { + paparazzi.accessibilitySnapshot { + PharmacyDetailsScreenFromMessagePreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromPharmacyScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromPharmacyScreenTest.kt new file mode 100644 index 00000000..4dcd3f03 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/components/PharmacyDetailsFromPharmacyScreenTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.components + +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacyPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PharmacyDetailsFromPharmacyScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = PharmacyPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, pharmacy -> + paparazzi.snapshot("parameter_$index") { + PharmacyDetailsScreenFromPharmacyPreview(pharmacy = pharmacy) + } + } + } +} + +class PharmacyDetailsFromPharmacyScreenAccessibilityTest(config: ScreenshotConfig) : BaseAccessibilityTest(config) { + + @Test + fun screenShotTest() { + val testParameters = PharmacyPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, pharmacy -> + paparazzi.accessibilitySnapshot("parameter_$index") { + PharmacyDetailsScreenFromPharmacyPreview(pharmacy = pharmacy) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchFilterScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchFilterScreenTest.kt new file mode 100644 index 00000000..6a56c3db --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchFilterScreenTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PharmacySearchFilterScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + paparazzi.snapshot { + PharmacyFilterSheetScreenPreview() + } + } +} + +class PharmacySearchFilterScreenAccessibilityTest(config: ScreenshotConfig) : BaseAccessibilityTest(config) { + @Test + fun screenShotTest() { + paparazzi.accessibilitySnapshot { + PharmacyFilterSheetScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreenTest.kt new file mode 100644 index 00000000..190b7b44 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchListScreenTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import de.gematik.ti.erp.app.pharmacy.ui.preview.PharmacySearchListScreenPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PharmacySearchListScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val testParameters = PharmacySearchListScreenPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, previewData -> + paparazzi.snapshot("parameter_$index") { + PharmacySearchListScreenContentPreview(previewData) + } + } + } +} + +class PharmacySearchListScreenAccessibilityTest(config: ScreenshotConfig) : BaseAccessibilityTest(config) { + @Test + fun screenShotTest() { + val testParameters = PharmacySearchListScreenPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, previewData -> + paparazzi.accessibilitySnapshot("parameter_$index") { + PharmacySearchListScreenContentPreview(previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchStartScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchStartScreenTest.kt new file mode 100644 index 00000000..284c91b2 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/ui/screens/PharmacySearchStartScreenTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.ui.screens + +import de.gematik.ti.erp.app.screenshot.BaseAccessibilityTest +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PharmacySearchStartScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + PharmacyStartScreenPreview() + } + } +} + +class PharmacySearchStartScreenAccessibilityTest(config: ScreenshotConfig) : BaseAccessibilityTest(config) { + + @Test + fun screenShotTest() { + paparazzi.accessibilitySnapshot { + PharmacyStartScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCaseTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCaseTest.kt new file mode 100644 index 00000000..a8b04b00 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCaseTest.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.Instant +import org.junit.After +import org.junit.Before +import org.junit.Test + +class GetOverviewPharmaciesUseCaseTest { + + private val repository: PharmacyRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var useCase: GetOverviewPharmaciesUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + coEvery { repository.loadOftenUsedPharmacies() } returns flowOf( + listOf( + OverviewPharmacy(Instant.parse("2024-08-01T10:00:00Z"), false, 10, "ID001", "Pharmacy B Often", "Address 2"), + OverviewPharmacy(Instant.parse("2024-08-06T09:00:00Z"), false, 5, "ID003", "Pharmacy C Often", "Address 3") + + ) + ) + coEvery { repository.loadFavoritePharmacies() } returns flowOf( + listOf( + OverviewPharmacy(Instant.parse("2024-08-02T11:00:00Z"), true, 20, "ID002", "Pharmacy A Fav", "Address 1"), + OverviewPharmacy(Instant.parse("2024-08-05T12:00:00Z"), true, 15, "ID004", "Pharmacy D Fav", "Address 4") + ) + ) + + useCase = GetOverviewPharmaciesUseCase(repository, dispatcher) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `sort the pharmacies by favorites first and then often used with last used in descending order`() { + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.size == 4) + assertEquals( + listOf( + "Pharmacy D Fav", + "Pharmacy A Fav", + "Pharmacy C Often", + "Pharmacy B Often" + ), + results + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when no favourites show the often used ones in descending order`() { + coEvery { repository.loadFavoritePharmacies() } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.size == 2) + assertEquals( + listOf( + "Pharmacy C Often", + "Pharmacy B Often" + ), + results + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when no often used one show the favourites in descending order`() { + coEvery { repository.loadOftenUsedPharmacies() } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.size == 2) + assertEquals( + listOf( + "Pharmacy D Fav", + "Pharmacy A Fav" + ), + results + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when no often used ones or no favourites, show nothing`() { + coEvery { repository.loadOftenUsedPharmacies() } returns flowOf(emptyList()) + coEvery { repository.loadFavoritePharmacies() } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.isEmpty()) + assertEquals( + emptyList(), + results + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when a often used one has the same telematik id as the favourite one, only consider the favourite one`() { + coEvery { repository.loadOftenUsedPharmacies() } returns flowOf( + listOf( + OverviewPharmacy(Instant.parse("2024-08-02T11:00:00Z"), false, 20, "ID002", "Pharmacy A Fav", "Address 1"), + OverviewPharmacy(Instant.parse("2024-08-06T09:00:00Z"), false, 5, "ID003", "Pharmacy C Often", "Address 3") + + ) + ) + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.size == 3) + assertEquals( + listOf( + "Pharmacy D Fav", + "Pharmacy A Fav", + "Pharmacy C Often" + ), + results + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when there are more than 5 favourites, only the first 5 favourites are considered`() { + coEvery { repository.loadFavoritePharmacies() } returns flowOf( + listOf( + OverviewPharmacy(Instant.parse("2024-08-02T11:00:00Z"), true, 20, "ID002", "Pharmacy A Fav", "Address 1"), + OverviewPharmacy(Instant.parse("2024-08-05T12:00:00Z"), true, 15, "ID004", "Pharmacy D Fav", "Address 4"), + OverviewPharmacy(Instant.parse("2024-08-06T12:00:00Z"), true, 15, "ID0041", "Pharmacy E Fav", "Address 4"), + OverviewPharmacy(Instant.parse("2024-08-07T12:00:00Z"), true, 15, "ID0042", "Pharmacy F Fav", "Address 4"), + OverviewPharmacy(Instant.parse("2024-08-08T12:00:00Z"), true, 15, "ID0043", "Pharmacy G Fav", "Address 4"), + OverviewPharmacy(Instant.parse("2024-08-09T12:00:00Z"), true, 15, "ID0044", "Pharmacy H Fav", "Address 4") + ) + ) + testScope.runTest { + advanceUntilIdle() + val results = useCase.invoke().first().map { it.pharmacyName } + assert(results.size == 5) + assertEquals( + listOf( + "Pharmacy H Fav", + "Pharmacy G Fav", + "Pharmacy F Fav", + "Pharmacy E Fav", + "Pharmacy D Fav" + ), + results + ) + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceControllerTest.kt new file mode 100644 index 00000000..01eb50b4 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/presentation/InvoiceControllerTest.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.presentation + +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.authentication.model.AuthenticationResult +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceError +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.invoice.usecase.DeleteAllLocalInvoices +import de.gematik.ti.erp.app.invoice.usecase.DeleteInvoiceUseCase +import de.gematik.ti.erp.app.invoice.usecase.DownloadInvoicesUseCase +import de.gematik.ti.erp.app.invoice.usecase.GetInvoiceByTaskIdUseCase +import de.gematik.ti.erp.app.invoice.usecase.GetInvoicesByProfileUseCase +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.mocks.TASK_ID +import de.gematik.ti.erp.app.mocks.invoice.model.mockPkvInvoiceRecord +import de.gematik.ti.erp.app.mocks.invoice.model.mockedInvoiceChargeItemBundle +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.pkv.usecase.ShareInvoiceUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.utils.compose.ComposableEvent +import de.gematik.ti.erp.app.utils.uistate.UiState +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.TimeZone +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.rules.TestWatcher + +class InvoiceControllerTest : TestWatcher() { + + private val invoiceRepository: InvoiceRepository = mockk() + private val profileRepository: ProfileRepository = mockk() + private val idpRepository: IdpRepository = mockk() + private val biometricAuthenticator = mockk() + + @OptIn(ExperimentalCoroutinesApi::class) + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(dispatcher) + + // events + private val askUserToLoginEvent: ComposableEvent = spyk(ComposableEvent()) + private val deleteSuccessfulEvent: ComposableEvent = spyk(ComposableEvent()) + private val downloadCompletedEvent: ComposableEvent = spyk(ComposableEvent()) + private val invoiceErrorEvent: ComposableEvent = spyk(ComposableEvent()) + private val getConsentEvent: ComposableEvent = spyk(ComposableEvent()) + private val showAuthenticationErrorDialog: ComposableEvent = spyk(ComposableEvent()) + + private lateinit var getProfileByIdUseCase: GetProfileByIdUseCase + private lateinit var getProfilesUseCase: GetProfilesUseCase + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase + private lateinit var downloadInvoiceUseCase: DownloadInvoicesUseCase + private lateinit var getInvoicesByProfileUseCase: GetInvoicesByProfileUseCase + private lateinit var getInvoiceByTaskIdUseCase: GetInvoiceByTaskIdUseCase + private lateinit var deleteInvoiceUseCase: DeleteInvoiceUseCase + private lateinit var deleteAllInvoicesUseCase: DeleteAllLocalInvoices + private lateinit var shareInvoiceUseCase: ShareInvoiceUseCase + + private lateinit var controllerUnderTest: InvoiceController + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + + getProfileByIdUseCase = GetProfileByIdUseCase(profileRepository, dispatcher) + getProfilesUseCase = GetProfilesUseCase(profileRepository, dispatcher) + getActiveProfileUseCase = GetActiveProfileUseCase(profileRepository, dispatcher) + chooseAuthenticationDataUseCase = + ChooseAuthenticationDataUseCase(profileRepository, idpRepository, dispatcher) + downloadInvoiceUseCase = DownloadInvoicesUseCase(profileRepository, invoiceRepository, dispatcher) + getInvoicesByProfileUseCase = GetInvoicesByProfileUseCase(invoiceRepository, TimeZone.of("UTC"), dispatcher) + getInvoiceByTaskIdUseCase = GetInvoiceByTaskIdUseCase(invoiceRepository, dispatcher) + deleteInvoiceUseCase = spyk(DeleteInvoiceUseCase(profileRepository, invoiceRepository, dispatcher)) + deleteAllInvoicesUseCase = DeleteAllLocalInvoices(invoiceRepository, dispatcher) + shareInvoiceUseCase = ShareInvoiceUseCase(invoiceRepository, dispatcher) + + controllerUnderTest = InvoiceController( + profileId = PROFILE_ID, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + downloadInvoicesUseCase = downloadInvoiceUseCase, + getInvoicesByProfileUseCase = getInvoicesByProfileUseCase, + getInvoiceByTaskIdUseCase = getInvoiceByTaskIdUseCase, + deleteInvoiceUseCase = deleteInvoiceUseCase, + deleteAllInvoicesUseCase = deleteAllInvoicesUseCase, + shareInvoiceUseCase = shareInvoiceUseCase, + biometricAuthenticator = biometricAuthenticator, + invoiceDetailScreenEvents = InvoiceDetailScreenEvents( + askUserToLoginEvent = askUserToLoginEvent, + deleteSuccessfulEvent = deleteSuccessfulEvent + ), + invoiceListScreenEvents = InvoiceListScreenEvents( + downloadCompletedEvent = downloadCompletedEvent, + invoiceErrorEvent = invoiceErrorEvent, + getConsentEvent = getConsentEvent, + showAuthenticationErrorDialog = showAuthenticationErrorDialog + ) + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `getting the list of invoices with loading state at start`() { + coEvery { profileRepository.getProfileById(PROFILE_ID) } returns flowOf(API_MOCK_PROFILE) + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord())) + coEvery { invoiceRepository.deleteLocalInvoice(TASK_ID) } returns Unit + + val result = mapOf(Year(2024) to listOf(mockPkvInvoiceRecord())) + testScope.runTest { + advanceUntilIdle() + val item = controllerUnderTest.invoices.first() + assertEquals(UiState.Data(result), item) + coVerify(exactly = 1) { getInvoicesByProfileUseCase.invoke(PROFILE_ID) } + coVerify(exactly = 1) { invoiceRepository.invoices(PROFILE_ID) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `deleting the invoice when the user is not logged in`() { + every { askUserToLoginEvent.trigger(Unit) } just Runs + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(false) + coEvery { invoiceRepository.deleteRemoteInvoiceById(TASK_ID, PROFILE_ID) } returns Result.success(Unit) + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deleteInvoice(TASK_ID, PROFILE_ID) + verify(exactly = 1) { askUserToLoginEvent.trigger(Unit) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `deleting the invoice and it returns an error`() { + val error = InvoiceError(HttpErrorState.Unknown) + every { invoiceErrorEvent.trigger(error) } just Runs + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(true) + coEvery { invoiceRepository.deleteRemoteInvoiceById(TASK_ID, PROFILE_ID) } returns Result.failure(InvoiceError(HttpErrorState.Unknown)) + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deleteInvoice(TASK_ID, PROFILE_ID) + verify(exactly = 1) { invoiceErrorEvent.trigger(error) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `deleting the invoice successfully`() { + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(true) + coEvery { invoiceRepository.deleteRemoteInvoiceById(TASK_ID, PROFILE_ID) } returns Result.success(Unit) + coEvery { invoiceRepository.deleteLocalInvoice(TASK_ID) } returns Unit + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deleteInvoice(TASK_ID, PROFILE_ID) + verify(exactly = 0) { invoiceErrorEvent.trigger(any()) } + verify(exactly = 0) { askUserToLoginEvent.trigger(Unit) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `downloading the invoice when the user is not logged in`() { + every { askUserToLoginEvent.trigger(Unit) } just Runs + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(false) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.downloadInvoices(PROFILE_ID) + verify(exactly = 1) { askUserToLoginEvent.trigger(Unit) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `downloading the invoices and it returns an error on no charge item bundle`() { + val error = InvoiceError(HttpErrorState.Unknown) + val latestTimestamp = "2024-01-01T00:00:00Z" + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(true) + coEvery { invoiceRepository.getLatestTimeStamp(PROFILE_ID) } returns flowOf(latestTimestamp) + coEvery { invoiceRepository.downloadChargeItemBundle(PROFILE_ID, latestTimestamp) } returns Result.failure(error) + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.downloadInvoices(PROFILE_ID) + verify(exactly = 1) { invoiceErrorEvent.trigger(error) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `downloading the invoices and it returns an error on no charge item from task id`() { + val error = InvoiceError(HttpErrorState.Unknown) + val latestTimestamp = "2024-01-01T00:00:00Z" + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(true) + coEvery { invoiceRepository.getLatestTimeStamp(PROFILE_ID) } returns flowOf(latestTimestamp) + coEvery { invoiceRepository.downloadChargeItemBundle(PROFILE_ID, latestTimestamp) } returns Result.success( + mockedInvoiceChargeItemBundle(listOf(TASK_ID)) + ) + coEvery { invoiceRepository.downloadChargeItemByTaskId(PROFILE_ID, TASK_ID) } returns Result.failure(error) + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.downloadInvoices(PROFILE_ID) + verify(exactly = 1) { invoiceErrorEvent.trigger(error) } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `downloading the invoices successfully`() { + val someJson: JsonElement = JsonPrimitive("success") + val latestTimestamp = "2024-01-01T00:00:00Z" + coEvery { profileRepository.isSsoTokenValid(PROFILE_ID) } returns flowOf(true) + coEvery { invoiceRepository.getLatestTimeStamp(PROFILE_ID) } returns flowOf(latestTimestamp) + coEvery { invoiceRepository.downloadChargeItemBundle(PROFILE_ID, latestTimestamp) } returns Result.success( + mockedInvoiceChargeItemBundle(listOf(TASK_ID)) + ) + coEvery { invoiceRepository.downloadChargeItemByTaskId(PROFILE_ID, TASK_ID) } returns Result.success(someJson) + coEvery { invoiceRepository.saveInvoice(PROFILE_ID, someJson) } returns Unit + coEvery { invoiceRepository.invoices(PROFILE_ID) } returns flowOf(listOf(mockPkvInvoiceRecord(PROFILE_ID))) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.downloadInvoices(PROFILE_ID) + verify(exactly = 0) { invoiceErrorEvent.trigger(any()) } + verify(exactly = 0) { askUserToLoginEvent.trigger(Unit) } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceCorrectionScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceCorrectionScreenTest.kt new file mode 100644 index 00000000..81e7b2e7 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceCorrectionScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui + +import de.gematik.ti.erp.app.pkv.ui.preview.InvoiceLocalCorrectionScreenPreviewParameterProvider +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceLocalCorrectionContentPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class InvoiceCorrectionScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = InvoiceLocalCorrectionScreenPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, invoice -> + paparazzi.snapshot("parameter_$index") { + InvoiceLocalCorrectionContentPreview(invoice = invoice) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreenScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreenScreenshotTest.kt new file mode 100644 index 00000000..3d99a215 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceShareScreenScreenshotTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pkv.ui + +import de.gematik.ti.erp.app.pkv.ui.screens.InvoiceShareScreenScaffoldPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class InvoiceShareScreenScreenshotTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + InvoiceShareScreenScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskDataIdControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskDataIdControllerTest.kt new file mode 100644 index 00000000..efc214db --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/GetPrescriptionByTaskDataIdControllerTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.presentation + +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.rules.TestWatcher + +class GetPrescriptionByTaskDataIdControllerTest : TestWatcher() { + + private val prescriptionRepository: PrescriptionRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: GetPrescriptionByTaskIdController + private lateinit var getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + + getPrescriptionByTaskIdUseCase = GetPrescriptionByTaskIdUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + + controllerUnderTest = GetPrescriptionByTaskIdController( + taskId = "taskId", + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `prescription is empty and screen in error state`() { + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.prescription.first() + assert(prescription.isErrorState) + } + } + + @Test + fun `prescription is loading and screen in loading state`() { + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + + testScope.runTest { + val prescription = controllerUnderTest.prescription.first() + assert(prescription.isLoadingState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `prescription is empty and screen in state`() { + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.prescription.first() + assert(prescription.isEmptyState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `a synced prescription is loaded and screen is in data state with a synced prescription`() { + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SYNCED_TASK) + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.prescription.first() + assert(prescription.isDataState) + assert(prescription.data is PrescriptionData.Synced) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `a scanned prescription is loaded and screen is in data state with a scanned prescription`() { + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.prescription.first() + assert(prescription.isDataState) + assert(prescription.data is PrescriptionData.Scanned) + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailControllerTest.kt new file mode 100644 index 00000000..55264ea0 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/presentation/PrescriptionDetailControllerTest.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.presentation + +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.fhir.model.json +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.medicationplan.usecase.LoadMedicationScheduleByTaskIdUseCase +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.usecase.DeletePrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetPrescriptionByTaskIdUseCase +import de.gematik.ti.erp.app.prescription.usecase.RedeemScannedTaskUseCase +import de.gematik.ti.erp.app.prescription.usecase.UpdateScannedTaskNameUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isEmptyState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.rules.TestWatcher + +class PrescriptionDetailControllerTest : TestWatcher() { + + private val prescriptionRepository: PrescriptionRepository = mockk() + private val featureToggleManager: FeatureToggleManager = mockk() + private val medicationPlanRepository: MedicationPlanRepository = mockk() + private val profileRepository: ProfileRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: PrescriptionDetailController + private lateinit var getPrescriptionByTaskIdUseCase: GetPrescriptionByTaskIdUseCase + private lateinit var redeemScannedTaskUseCase: RedeemScannedTaskUseCase + private lateinit var deletePrescriptionUseCase: DeletePrescriptionUseCase + private lateinit var updateScannedTaskNameUseCase: UpdateScannedTaskNameUseCase + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var loadMedicationScheduleByTaskIdUseCase: LoadMedicationScheduleByTaskIdUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + + getPrescriptionByTaskIdUseCase = GetPrescriptionByTaskIdUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + loadMedicationScheduleByTaskIdUseCase = spyk( + LoadMedicationScheduleByTaskIdUseCase( + medicationPlanRepository = medicationPlanRepository, + dispatcher = dispatcher + ) + ) + redeemScannedTaskUseCase = spyk( + RedeemScannedTaskUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + ) + deletePrescriptionUseCase = DeletePrescriptionUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + updateScannedTaskNameUseCase = spyk( + UpdateScannedTaskNameUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + ) + getActiveProfileUseCase = spyk( + GetActiveProfileUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + ) + every { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(null) + every { featureToggleManager.isFeatureEnabled(any()) } returns flowOf(true) + controllerUnderTest = PrescriptionDetailController( + getActiveProfileUseCase = getActiveProfileUseCase, + taskId = "taskId", + redeemScannedTaskUseCase = redeemScannedTaskUseCase, + deletePrescriptionUseCase = deletePrescriptionUseCase, + loadMedicationScheduleByTaskIdUseCase = loadMedicationScheduleByTaskIdUseCase, + getPrescriptionByTaskIdUseCase = getPrescriptionByTaskIdUseCase, + updateScannedTaskNameUseCase = updateScannedTaskNameUseCase, + featureToggleManager = featureToggleManager + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `active profile is loaded and prescription is empty screen in error state`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.profilePrescription.first() + assert(prescription.isErrorState) + } + } + + @Test + fun `profilePrescription is loading and screen in loading state`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + + testScope.runTest { + val prescription = controllerUnderTest.profilePrescription.first() + assert(prescription.isLoadingState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `prescription is empty and screen in empty state`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.profilePrescription.first() + assert(prescription.isEmptyState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `a synced prescription is loaded and screen is in data state with a synced prescription`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SYNCED_TASK) + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf() + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.profilePrescription.first() + assert(prescription.isDataState) + assert(prescription.data?.second is PrescriptionData.Synced) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `a scanned prescription is loaded and screen is in data state with a scanned prescription`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf() + every { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(API_ACTIVE_SCANNED_TASK) + + testScope.runTest { + advanceUntilIdle() + val prescription = controllerUnderTest.profilePrescription.first() + assert(prescription.isDataState) + assert(prescription.data?.second is PrescriptionData.Scanned) + } + } + + @Test + fun `redeem scanned task (false) should invoke the redeemScannedTaskUseCase and prescriptionRepository`() { + coEvery { prescriptionRepository.updateRedeemedOn(any(), any()) } returns Unit + testScope.runTest { + controllerUnderTest.redeemScannedTask("taskId", false) + } + coVerify(exactly = 1) { + redeemScannedTaskUseCase.invoke("taskId", false) + } + coVerify(exactly = 1) { + prescriptionRepository.updateRedeemedOn( + "taskId", + null + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `redeem scanned task (true) should invoke the redeemScannedTaskUseCase`() { + coEvery { prescriptionRepository.updateRedeemedOn(any(), any()) } returns Unit + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.redeemScannedTask("taskId", true) + } + coVerify(exactly = 1) { + redeemScannedTaskUseCase.invoke("taskId", true) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Delete prescription should invoke the deletePrescriptionUseCase and remove data in the repository`() { + coEvery { + prescriptionRepository.deleteRemoteTaskById(any(), any()) + } returns Result.success(json.parseToJsonElement("{}")) + coEvery { prescriptionRepository.deleteLocalTaskById(any()) } returns Unit + coEvery { prescriptionRepository.deleteLocalInvoicesById(any()) } returns Unit + coEvery { prescriptionRepository.wasProfileEverAuthenticated(any()) } returns true + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deletePrescription("profileId", "taskId") + } + coVerify(exactly = 1) { + deletePrescriptionUseCase.invoke(profileId = "profileId", taskId = "taskId", deleteLocallyOnly = false) + } + coVerify(exactly = 1) { prescriptionRepository.deleteRemoteTaskById("profileId", "taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalTaskById("taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalInvoicesById("taskId") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Delete prescription on a unauthenticated profile should only remove local prescription`() { + coEvery { + prescriptionRepository.deleteRemoteTaskById(any(), any()) + } returns Result.success(json.parseToJsonElement("{}")) + coEvery { prescriptionRepository.deleteLocalTaskById(any()) } returns Unit + coEvery { prescriptionRepository.deleteLocalInvoicesById(any()) } returns Unit + coEvery { prescriptionRepository.wasProfileEverAuthenticated(any()) } returns false + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deletePrescription("profileId", "taskId") + } + coVerify(exactly = 1) { + deletePrescriptionUseCase.invoke(profileId = "profileId", taskId = "taskId", deleteLocallyOnly = false) + } + coVerify(exactly = 0) { prescriptionRepository.deleteRemoteTaskById("profileId", "taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalTaskById("taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalInvoicesById("taskId") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Delete prescription locally should only remove local prescription and invoices`() { + coEvery { prescriptionRepository.deleteLocalTaskById(any()) } returns Unit + coEvery { prescriptionRepository.deleteLocalInvoicesById(any()) } returns Unit + coEvery { prescriptionRepository.wasProfileEverAuthenticated(any()) } returns true + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deletePrescriptionFromLocal("profileId", "taskId") + } + coVerify(exactly = 1) { + deletePrescriptionUseCase.invoke(profileId = "profileId", taskId = "taskId", deleteLocallyOnly = false) + } + coVerify(exactly = 0) { prescriptionRepository.deleteRemoteTaskById("profileId", "taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalTaskById("taskId") } + coVerify(exactly = 1) { prescriptionRepository.deleteLocalInvoicesById("taskId") } + } + + @Test + fun `Update scanned task name should only invoke the useCase and repository`() { + coEvery { prescriptionRepository.updateScannedTaskName(any(), any()) } returns Unit + + testScope.runTest { + controllerUnderTest.updateScannedTaskName("taskId", "newName") + } + coVerify(exactly = 1) { + updateScannedTaskNameUseCase.invoke(taskId = "taskId", name = "newName") + } + coVerify(exactly = 1) { prescriptionRepository.updateScannedTaskName("taskId", "newName") } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreenTest.kt new file mode 100644 index 00000000..9ad80b22 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailAccidentInfoScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import de.gematik.ti.erp.app.prescription.detail.ui.preview.AccidentInfoPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PrescriptionDetailAccidentInfoScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val accidentInfoPreviewParameters = AccidentInfoPreviewParameterProvider().values.toList() + accidentInfoPreviewParameters.forEach { data -> + paparazzi.snapshot(data.name) { + PrescriptionDetailAccidentInfoScreenPreview(data) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenTest.kt new file mode 100644 index 00000000..8ab7773c --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import de.gematik.ti.erp.app.prescription.detail.ui.preview.PrescriptionDetailPreviewParameter +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PrescriptionDetailScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val prescriptionDetailPreviews = PrescriptionDetailPreviewParameter().values.toList() + prescriptionDetailPreviews.forEach { prescriptionDetailPreview -> + paparazzi.snapshot(prescriptionDetailPreview.name) { + PrescriptionDetailScreenPreview(prescriptionDetailPreview) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsControllerTest.kt new file mode 100644 index 00000000..cb4ad91b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/presentation/PrescriptionsControllerTest.kt @@ -0,0 +1,607 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.presentation + +import app.cash.turbine.test +import de.gematik.ti.erp.app.analytics.tracker.Tracker +import de.gematik.ti.erp.app.authentication.presentation.BiometricAuthenticator +import de.gematik.ti.erp.app.base.NetworkStatusTracker +import de.gematik.ti.erp.app.base.model.DownloadResourcesState +import de.gematik.ti.erp.app.base.model.DownloadResourcesState.NotStarted +import de.gematik.ti.erp.app.base.usecase.DownloadAllResourcesUseCase +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.consent.usecase.ShowGrantConsentDrawerUseCase +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.idp.usecase.ChooseAuthenticationDataUseCase +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ACTIVE_SYNCED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ARCHIVE_SCANNED_TASK +import de.gematik.ti.erp.app.mocks.prescription.api.API_ARCHIVE_SYNCED_TASK +import de.gematik.ti.erp.app.mocks.prescription.model.MODEL_SCANNED_PRESCRIPTION_ACTIVE +import de.gematik.ti.erp.app.mocks.prescription.model.MODEL_SCANNED_PRESCRIPTION_ARCHIVED +import de.gematik.ti.erp.app.mocks.prescription.model.MODEL_SYNCED_PRESCRIPTION_ACTIVE +import de.gematik.ti.erp.app.mocks.prescription.model.MODEL_SYNCED_PRESCRIPTION_ARCHIVE +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_WITH_SSO_TOKEN_PROFILE +import de.gematik.ti.erp.app.mocks.settings.api.SETTINGS_DATA_GENERAL +import de.gematik.ti.erp.app.prescription.repository.DownloadResourcesStateRepository +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.prescription.usecase.GetActivePrescriptionsUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetArchivedPrescriptionsUseCase +import de.gematik.ti.erp.app.prescription.usecase.GetDownloadResourcesSnapshotStateUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.redeem.usecase.HasRedeemableTasksUseCase +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.usecase.GetCanStartToolTipsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveToolTipsShownUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.rules.TestWatcher +import kotlin.test.assertEquals + +class PrescriptionsControllerTest : TestWatcher() { + + private val profileRepository: ProfileRepository = mockk() + private val idpRepository: IdpRepository = mockk() + private val taskRepository: TaskRepository = mockk() + private val communicationRepository: CommunicationRepository = mockk() + private val invoicesRepository: InvoiceRepository = mockk() + private val downloadResourcesStateRepository: DownloadResourcesStateRepository = mockk() + private val prescriptionRepository: PrescriptionRepository = mockk() + private val settingsRepository: SettingsRepository = mockk() + private val consentRepository: ConsentRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val biometricAuthenticator = mockk() + private val networkStatusTracker = mockk() + private val tracker = mockk() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: PrescriptionsController + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var getActivePrescriptionsUseCase: GetActivePrescriptionsUseCase + private lateinit var getArchivedPrescriptionsUseCase: GetArchivedPrescriptionsUseCase + private lateinit var getMLKitAcceptedUseCase: GetMLKitAcceptedUseCase + private lateinit var getShowWelcomeDrawerUseCase: GetShowWelcomeDrawerUseCase + private lateinit var getCanStartToolTipsUseCase: GetCanStartToolTipsUseCase + private lateinit var saveToolTipsShownUseCase: SaveToolTipsShownUseCase + private lateinit var showGrantConsentDrawerUseCase: ShowGrantConsentDrawerUseCase + private lateinit var chooseAuthenticationDataUseCase: ChooseAuthenticationDataUseCase + private lateinit var getProfileByIdUseCase: GetProfileByIdUseCase + private lateinit var getProfilesUseCase: GetProfilesUseCase + private lateinit var switchActiveProfileUseCase: SwitchActiveProfileUseCase + private lateinit var downloadAllResourcesUseCase: DownloadAllResourcesUseCase + private lateinit var snapshotStateUseCase: GetDownloadResourcesSnapshotStateUseCase + private lateinit var hasRedeemableTasksUseCase: HasRedeemableTasksUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + + val snapshotState = MutableSharedFlow(replay = 1).asSharedFlow() + val detailState = MutableStateFlow(NotStarted).asStateFlow() + + every { settingsRepository.general } returns flowOf(SETTINGS_DATA_GENERAL) + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { downloadResourcesStateRepository.snapshotState() } returns snapshotState + every { downloadResourcesStateRepository.detailState() } returns detailState + + getActiveProfileUseCase = GetActiveProfileUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + getActivePrescriptionsUseCase = GetActivePrescriptionsUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + getArchivedPrescriptionsUseCase = GetArchivedPrescriptionsUseCase( + repository = prescriptionRepository, + dispatcher = dispatcher + ) + getMLKitAcceptedUseCase = GetMLKitAcceptedUseCase( + settingsRepository = settingsRepository, + dispatcher = dispatcher + ) + getShowWelcomeDrawerUseCase = GetShowWelcomeDrawerUseCase( + settingsRepository = settingsRepository, + dispatcher = dispatcher + ) + getCanStartToolTipsUseCase = GetCanStartToolTipsUseCase( + settingsRepository = settingsRepository, + dispatcher = dispatcher + ) + saveToolTipsShownUseCase = SaveToolTipsShownUseCase( + settingsRepository = settingsRepository, + dispatcher = dispatcher + ) + showGrantConsentDrawerUseCase = ShowGrantConsentDrawerUseCase( + consentRepository = consentRepository, + profilesRepository = profileRepository + ) + chooseAuthenticationDataUseCase = ChooseAuthenticationDataUseCase( + profileRepository = profileRepository, + idpRepository = idpRepository, + dispatcher = dispatcher + ) + getProfileByIdUseCase = GetProfileByIdUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + getProfilesUseCase = GetProfilesUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + switchActiveProfileUseCase = SwitchActiveProfileUseCase( + repository = profileRepository, + dispatcher = dispatcher + ) + downloadAllResourcesUseCase = DownloadAllResourcesUseCase( + profileRepository = profileRepository, + taskRepository = taskRepository, + communicationRepository = communicationRepository, + invoicesRepository = invoicesRepository, + stateRepository = downloadResourcesStateRepository, + networkStatusTracker = networkStatusTracker, + dispatcher = dispatcher + ) + snapshotStateUseCase = GetDownloadResourcesSnapshotStateUseCase( + downloadResourcesStateRepository = downloadResourcesStateRepository + ) + hasRedeemableTasksUseCase = HasRedeemableTasksUseCase( + prescriptionRepository = prescriptionRepository, + dispatchers = dispatcher + ) + + controllerUnderTest = PrescriptionsController( + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + snapshotStateUseCase = snapshotStateUseCase, + biometricAuthenticator = biometricAuthenticator, + getActiveProfileUseCase = getActiveProfileUseCase, + downloadAllResourcesUseCase = downloadAllResourcesUseCase, + activePrescriptionsUseCase = getActivePrescriptionsUseCase, + archivedPrescriptionsUseCase = getArchivedPrescriptionsUseCase, + getMLKitAcceptedUseCase = getMLKitAcceptedUseCase, + getShowWelcomeDrawerUseCase = getShowWelcomeDrawerUseCase, + showGrantConsentDrawerUseCase = showGrantConsentDrawerUseCase, + saveToolTipsShownUseCase = saveToolTipsShownUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + hasRedeemableTasksUseCase = hasRedeemableTasksUseCase, + tracker = tracker + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `profile is empty and screen in error state`() { + every { profileRepository.activeProfile() } throws Exception("Error") + + testScope.runTest { + advanceUntilIdle() + val profile = controllerUnderTest.activeProfile.first() + assert(profile.isErrorState) + } + } + + @Test + fun `profile is loading and screen in loading state`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + + testScope.runTest { + controllerUnderTest.activeProfile.test { + val profile = awaitItem() + println(profile) + assert(profile.isLoadingState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `profile is loaded and screen in content state with no prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + testScope.runTest { + advanceUntilIdle() + val profile = controllerUnderTest.activeProfile.first() + assert(profile.isDataState) + controllerUnderTest.activePrescriptions.test { + val initialEmittedState = awaitItem() + assertEquals(UiState.Loading(), initialEmittedState) + val finalEmittedState = awaitItem() + assertEquals(UiState.Data(emptyList()), finalEmittedState) + } + controllerUnderTest.archivedPrescriptions.test { + val initialEmittedState = awaitItem() + assertEquals(UiState.Loading(), initialEmittedState) + val finalEmittedState = awaitItem() + assertEquals(UiState.Data(null), finalEmittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `event is sent that one new prescriptions is loaded`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_WITH_SSO_TOKEN_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + val controller = spyk(controllerUnderTest) + testScope.runTest { + advanceUntilIdle() + // one new prescription is loaded from the backend which makes it available for the user + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SYNCED_TASK)) + controller.refreshDownload() + advanceTimeBy(1000) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only active scanned prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SCANNED_PRESCRIPTION_ACTIVE)), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(null), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only active synced prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SYNCED_TASK)) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ACTIVE)), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(null), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only archived scanned prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ARCHIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(emptyList()), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SCANNED_PRESCRIPTION_ARCHIVED)), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only archived synced prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ARCHIVE_SYNCED_TASK)) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(emptyList()), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ARCHIVE)), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only synced prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf( + listOf(API_ACTIVE_SYNCED_TASK, API_ARCHIVE_SYNCED_TASK) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ACTIVE)), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ARCHIVE)), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with only scanned prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf( + listOf(API_ACTIVE_SCANNED_TASK, API_ARCHIVE_SCANNED_TASK) + ) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SCANNED_PRESCRIPTION_ACTIVE)), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SCANNED_PRESCRIPTION_ARCHIVED)), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `loaded with synced and scanned prescriptions`() { + every { profileRepository.activeProfile() } returns flowOf(API_MOCK_PROFILE) + every { prescriptionRepository.scannedTasks(any()) } returns flowOf( + listOf(API_ACTIVE_SCANNED_TASK, API_ARCHIVE_SCANNED_TASK) + ) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf( + listOf(API_ACTIVE_SYNCED_TASK, API_ARCHIVE_SYNCED_TASK) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.activePrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ACTIVE, MODEL_SCANNED_PRESCRIPTION_ACTIVE)), emittedState) + } + controllerUnderTest.archivedPrescriptions.test { + awaitItem() // initial emit should be always loading + val emittedState = awaitItem() + assertEquals(UiState.Data(listOf(MODEL_SYNCED_PRESCRIPTION_ARCHIVE, MODEL_SCANNED_PRESCRIPTION_ARCHIVED)), emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `ml-kit acceptance test`() { + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + testScope.runTest { + advanceUntilIdle() + assert(!controllerUnderTest.isMLKitAccepted.first()) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `get the state to check if the welcome drawer needs to be shown`() { + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.shouldShowWelcomeDrawer.test { + val emittedState = awaitItem() + awaitComplete() + assertEquals(true, emittedState) + } + } + verify(atMost = 1) { getShowWelcomeDrawerUseCase() } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `get the state to check if the welcome drawer is already shown`() { + every { settingsRepository.general } returns flowOf(SETTINGS_DATA_GENERAL.copy(welcomeDrawerShown = true)) + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + val showWelcomeDrawerUseCase = GetShowWelcomeDrawerUseCase(settingsRepository, dispatcher) + + val controller = PrescriptionsController( + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + snapshotStateUseCase = snapshotStateUseCase, + biometricAuthenticator = biometricAuthenticator, + getActiveProfileUseCase = getActiveProfileUseCase, + downloadAllResourcesUseCase = downloadAllResourcesUseCase, + activePrescriptionsUseCase = getActivePrescriptionsUseCase, + archivedPrescriptionsUseCase = getArchivedPrescriptionsUseCase, + getMLKitAcceptedUseCase = getMLKitAcceptedUseCase, + getShowWelcomeDrawerUseCase = showWelcomeDrawerUseCase, + showGrantConsentDrawerUseCase = showGrantConsentDrawerUseCase, + saveToolTipsShownUseCase = saveToolTipsShownUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + hasRedeemableTasksUseCase = hasRedeemableTasksUseCase, + tracker = tracker + ) + + testScope.runTest { + advanceUntilIdle() + controller.shouldShowWelcomeDrawer.test { + val emittedState = awaitItem() + awaitComplete() + assertEquals(false, emittedState) + } + } + // one call is from setup, the other one is from the test + verify(atMost = 2) { getShowWelcomeDrawerUseCase() } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `test tracking`() { + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SCANNED_TASK, API_ARCHIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SYNCED_TASK, API_ARCHIVE_SYNCED_TASK)) + coEvery { tracker.trackEvent(any()) } returns Unit + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.trackPrescriptionCounts() + } + + coVerify(exactly = 3) { tracker.trackEvent(any()) } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `check if archive is not empty`() { + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SCANNED_TASK, API_ARCHIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SYNCED_TASK, API_ARCHIVE_SYNCED_TASK)) + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.isArchiveEmpty.test { + awaitItem() + val emittedState = awaitItem() + assertEquals(false, emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `check if archive is empty as the initial state`() { + every { prescriptionRepository.scannedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SCANNED_TASK)) + every { prescriptionRepository.syncedTasks(any()) } returns flowOf(listOf(API_ACTIVE_SYNCED_TASK)) + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.isArchiveEmpty.test { + val emittedState = awaitItem() + assertEquals(true, emittedState) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `save the state of the tooltips once the user has accepted it`() { + coJustRun { settingsRepository.saveMainScreenTooltipShown() } + coEvery { prescriptionRepository.scannedTasks(any()) } returns flowOf(emptyList()) + coEvery { prescriptionRepository.syncedTasks(any()) } returns flowOf(emptyList()) + + val saveToolTipsShownUseCase = spyk(SaveToolTipsShownUseCase(settingsRepository, dispatcher)) + val controllerUnderTest = PrescriptionsController( + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + chooseAuthenticationDataUseCase = chooseAuthenticationDataUseCase, + snapshotStateUseCase = snapshotStateUseCase, + biometricAuthenticator = biometricAuthenticator, + getActiveProfileUseCase = getActiveProfileUseCase, + downloadAllResourcesUseCase = downloadAllResourcesUseCase, + activePrescriptionsUseCase = getActivePrescriptionsUseCase, + archivedPrescriptionsUseCase = getArchivedPrescriptionsUseCase, + getMLKitAcceptedUseCase = getMLKitAcceptedUseCase, + getShowWelcomeDrawerUseCase = getShowWelcomeDrawerUseCase, + showGrantConsentDrawerUseCase = showGrantConsentDrawerUseCase, + saveToolTipsShownUseCase = saveToolTipsShownUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + hasRedeemableTasksUseCase = hasRedeemableTasksUseCase, + tracker = tracker + ) + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.saveToolTipsShown() + } + coVerify(exactly = 1) { saveToolTipsShownUseCase.invoke() } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionStateTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionStateTest.kt new file mode 100644 index 00000000..9f0ee02f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionStateTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui + +import de.gematik.ti.erp.app.prescription.ui.components.PrescriptionStateInfosCombinedPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PrescriptionStateTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + paparazzi.snapshot { + PrescriptionStateInfosCombinedPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreenScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreenScreenshotTest.kt new file mode 100644 index 00000000..7936eeac --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/GrantConsentBottomSheetScreenScreenshotTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.screen + +import de.gematik.ti.erp.app.screenshot.BaseComponentScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class GrantConsentBottomSheetScreenScreenshotTest(config: ScreenshotConfig) : BaseComponentScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + GrantConsentBottomSheetScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreenTest.kt new file mode 100644 index 00000000..32a85645 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.screen + +import de.gematik.ti.erp.app.prescription.ui.preview.PrescriptionScreenPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PrescriptionScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val params = PrescriptionScreenPreviewParameterProvider().values.toList() + params.forEach { item -> + paparazzi.snapshot( + name = "PrescriptionScreen_${item.name}" + ) { + PrescriptionsScreenScaffoldPreview(data = item) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionsArchiveScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionsArchiveScreenTest.kt new file mode 100644 index 00000000..5585f524 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/PrescriptionsArchiveScreenTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.screen + +import de.gematik.ti.erp.app.prescription.ui.preview.PrescriptionsArchiveScreenPreviewParameterProvider +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class PrescriptionsArchiveScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val params = PrescriptionsArchiveScreenPreviewParameterProvider().values.toList() + params.forEachIndexed { index, previewData -> + paparazzi.snapshot("parameter_$index") { + PrescriptionsArchiveScreenPreview(previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreenTest.kt new file mode 100644 index 00000000..dcadaa79 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/prescription/ui/screen/WelcomeDrawerBottomSheetScreenTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.ui.screen + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class WelcomeDrawerBottomSheetScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + WelcomeDrawerScreenContentPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/GetProfileByIdControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/GetProfileByIdControllerTest.kt new file mode 100644 index 00000000..07e76a25 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/GetProfileByIdControllerTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import de.gematik.ti.erp.app.base.presentation.GetProfileByIdController +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isDataState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isErrorState +import de.gematik.ti.erp.app.utils.uistate.UiState.Companion.isLoadingState +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +class GetProfileByIdControllerTest { + + private val profileRepository: ProfileRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: GetProfileByIdController + private lateinit var getProfileByIdUseCase: GetProfileByIdUseCase + private lateinit var getProfilesUseCase: GetProfilesUseCase + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + getProfileByIdUseCase = GetProfileByIdUseCase(profileRepository, dispatcher) + getProfilesUseCase = GetProfilesUseCase(profileRepository, dispatcher) + getActiveProfileUseCase = GetActiveProfileUseCase(profileRepository, dispatcher) + controllerUnderTest = object : GetProfileByIdController( + PROFILE_ID, + getProfileByIdUseCase, + getProfilesUseCase, + getActiveProfileUseCase + ) {} + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `selected profile is empty and screen in error state`() { + every { profileRepository.getProfileById(PROFILE_ID) } returns emptyFlow() + every { profileRepository.profiles() } returns flowOf(listOf()) + + testScope.runTest { + advanceUntilIdle() + val combinedProfile = controllerUnderTest.combinedProfile.first() + assert(combinedProfile.isErrorState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `getProfiles is null and screen in error state`() { + every { profileRepository.getProfileById(PROFILE_ID) } returns flowOf(API_MOCK_PROFILE) + every { profileRepository.profiles() } returns emptyFlow() + + testScope.runTest { + advanceUntilIdle() + val combinedProfile = controllerUnderTest.combinedProfile.first() + assert(combinedProfile.isErrorState) + } + } + + @Test + fun `combined profile is loading and screen in loading state`() { + every { profileRepository.getProfileById(PROFILE_ID) } returns emptyFlow() + every { profileRepository.profiles() } returns emptyFlow() + + testScope.runTest { + val profile = controllerUnderTest.combinedProfile.value + assert(profile.isLoadingState) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `selected profile is successfully loaded`() { + every { profileRepository.getProfileById(PROFILE_ID) } returns flowOf(API_MOCK_PROFILE) + every { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + + testScope.runTest { + advanceUntilIdle() + val profile = controllerUnderTest.combinedProfile.first() + assert(profile.isDataState) + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenControllerTest.kt new file mode 100644 index 00000000..068fabdc --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileScreenControllerTest.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.mocks.PROFILE_ID +import de.gematik.ti.erp.app.mocks.profile.api.API_MOCK_PROFILE +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfileByIdUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ProfileScreenControllerTest { + + private val profileRepository: ProfileRepository = mockk() + private val idpRepository: IdpRepository = mockk() + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var controllerUnderTest: ProfileScreenController + + // tested by CombineSelectedProfileWithAllProfilesControllerTest + private lateinit var getProfileByIdUseCase: GetProfileByIdUseCase + + // tested by CombineSelectedProfileWithAllProfilesControllerTest + private lateinit var getProfilesUseCase: GetProfilesUseCase + private lateinit var addProfileUseCase: AddProfileUseCase + private lateinit var deleteProfileUseCase: DeleteProfileUseCase + private lateinit var logoutProfileUseCase: LogoutProfileUseCase + private lateinit var switchActiveProfileUseCase: SwitchActiveProfileUseCase + private lateinit var updateProfileUseCase: UpdateProfileUseCase + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + MockKAnnotations.init(this) + getProfileByIdUseCase = GetProfileByIdUseCase(profileRepository, dispatcher) + getProfilesUseCase = GetProfilesUseCase(profileRepository, dispatcher) + addProfileUseCase = spyk(AddProfileUseCase(profileRepository, dispatcher)) + deleteProfileUseCase = spyk(DeleteProfileUseCase(profileRepository, idpRepository, dispatcher)) + logoutProfileUseCase = spyk(LogoutProfileUseCase(idpRepository, dispatcher)) + switchActiveProfileUseCase = spyk(SwitchActiveProfileUseCase(profileRepository, dispatcher)) + updateProfileUseCase = spyk(UpdateProfileUseCase(profileRepository, dispatcher)) + getActiveProfileUseCase = spyk(GetActiveProfileUseCase(profileRepository, dispatcher)) + controllerUnderTest = ProfileScreenController( + profileId = PROFILE_ID, + getProfileByIdUseCase = getProfileByIdUseCase, + getProfilesUseCase = getProfilesUseCase, + addProfileUseCase = addProfileUseCase, + deleteProfileUseCase = deleteProfileUseCase, + logoutProfileUseCase = logoutProfileUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + updateProfileUseCase = updateProfileUseCase, + getActiveProfileUseCase = getActiveProfileUseCase + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `adding a profile should invoke the addProfileUseCase and create a new profile in the repository`() { + every { profileRepository.getProfileById(any()) } returns emptyFlow() + coEvery { profileRepository.createNewProfile(any()) } returns Unit + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.addNewProfile("newProfile") + } + coVerify(exactly = 1) { addProfileUseCase.invoke("newProfile") } + coVerify(exactly = 1) { profileRepository.createNewProfile("newProfile") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `switch a profile should invoke the switchActiveProfileUseCase and activate the profile in the repository`() { + coEvery { profileRepository.activateProfile("profileId") } returns Unit + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.switchActiveProfile("profileId") + } + coVerify(exactly = 1) { switchActiveProfileUseCase.invoke("profileId") } + coVerify(exactly = 1) { profileRepository.activateProfile("profileId") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `logout profile should invoke the logoutProfileUseCase and invalidate idp data in the repository`() { + coEvery { idpRepository.invalidate(any()) } returns Unit + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.logout("profileId") + } + coVerify(exactly = 1) { logoutProfileUseCase.invoke("profileId") } + coVerify(exactly = 1) { idpRepository.invalidate("profileId") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `update profile name should invoke the updateProfileUseCase and update data in the repository`() { + every { profileRepository.getProfileById(any()) } returns flowOf(API_MOCK_PROFILE) + every { profileRepository.profiles() } returns flowOf(listOf(API_MOCK_PROFILE)) + coEvery { profileRepository.updateProfileName(any(), any()) } returns Unit + + val profileId = API_MOCK_PROFILE.id + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.updateProfileName("newName") + } + coVerify(exactly = 1) { + updateProfileUseCase.invoke( + modifier = UpdateProfileUseCase.Companion.ProfileModifier.Name("newName"), + id = profileId + ) + } + coVerify(exactly = 1) { profileRepository.updateProfileName(profileId, "newName") } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `Delete profile should invoke the deleteProfileUseCase, invalidate and remove the data in the repositories`() { + coEvery { profileRepository.removeProfile(any(), any()) } returns Unit + coEvery { idpRepository.invalidateDecryptedAccessToken(any()) } returns Unit + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.deleteProfile("profileId", "name") + } + coVerify(exactly = 1) { + deleteProfileUseCase.invoke("profileId", "name") + } + coVerify(exactly = 1) { idpRepository.invalidateDecryptedAccessToken("profileId") } + coVerify(exactly = 1) { profileRepository.removeProfile("profileId", "name") } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditNameBottomSheetContentTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditNameBottomSheetContentTest.kt new file mode 100644 index 00000000..3b2ce88c --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileEditNameBottomSheetContentTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui + +import de.gematik.ti.erp.app.profiles.ui.preview.ProfilePreviewParameterProvider +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileEditNameBottomSheetScreenContentPreview +import de.gematik.ti.erp.app.screenshot.BaseComponentScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class ProfileEditNameBottomSheetContentTest(config: ScreenshotConfig) : BaseComponentScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = ProfilePreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, profileEditState -> + paparazzi.snapshot("parameter_$index") { + ProfileEditNameBottomSheetScreenContentPreview(profileEditState) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreenTest.kt new file mode 100644 index 00000000..6cade9e1 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfilePairedDevicesScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui + +import de.gematik.ti.erp.app.profiles.ui.preview.PairedDevicesPreviewParameterProvider +import de.gematik.ti.erp.app.profiles.ui.screens.ProfilePairedDevicesScreenScaffoldPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class ProfilePairedDevicesScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = PairedDevicesPreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, profileState -> + paparazzi.snapshot("parameter_$index") { + ProfilePairedDevicesScreenScaffoldPreview(profileState) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreenTest.kt new file mode 100644 index 00000000..d9738843 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.ui + +import de.gematik.ti.erp.app.profiles.ui.preview.ProfileStatePreviewParameterProvider +import de.gematik.ti.erp.app.profiles.ui.screens.ProfileScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class ProfileScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val testParameters = ProfileStatePreviewParameterProvider().values.toList() + testParameters.forEachIndexed { index, profileState -> + paparazzi.snapshot("parameter_$index") { + ProfileScreenPreview(profileState) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/HowToRadeemScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/HowToRadeemScreenTest.kt new file mode 100644 index 00000000..0025b38d --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/HowToRadeemScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem + +import de.gematik.ti.erp.app.redeem.ui.screens.HowToRedeemScaffoldScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class HowToRedeemScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + HowToRedeemScaffoldScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/LocalRedeemScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/LocalRedeemScreenTest.kt new file mode 100644 index 00000000..4f525613 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/LocalRedeemScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem + +import de.gematik.ti.erp.app.redeem.ui.preview.LocalRedeemPreviewParameter +import de.gematik.ti.erp.app.redeem.ui.screens.LocalRedeemScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class LocalRedeemScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val localRedeemScreenPreviews = LocalRedeemPreviewParameter().values.toList() + localRedeemScreenPreviews.forEachIndexed { index, localRedeemPreview -> + paparazzi.snapshot("parameter_$index ${localRedeemPreview.name}") { + LocalRedeemScreenPreview(localRedeemPreview) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/OnlineRedeemPreferencesScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/OnlineRedeemPreferencesScreenTest.kt new file mode 100644 index 00000000..43bf2dc6 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/OnlineRedeemPreferencesScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem + +import de.gematik.ti.erp.app.prescription.ui.preview.OnlineRedeemPreferencesScreenPreviewParameterProvider +import de.gematik.ti.erp.app.redeem.ui.screens.OnlineRedeemPreferencesScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class OnlineRedeemPreferencesScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val onlineRedeemPreferencesPreviews = OnlineRedeemPreferencesScreenPreviewParameterProvider().values.toList() + onlineRedeemPreferencesPreviews.forEachIndexed { index, prescription -> + paparazzi.snapshot("parameter_$index") { + OnlineRedeemPreferencesScreenPreview(prescription) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/RedeemEditShippingScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/RedeemEditShippingScreenTest.kt new file mode 100644 index 00000000..6e7f8775 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/RedeemEditShippingScreenTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem + +import de.gematik.ti.erp.app.redeem.ui.preview.RedeemEditShippingPreviewParameter +import de.gematik.ti.erp.app.redeem.ui.screens.RedeemEditShippingScreenPreview +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import org.junit.Test + +class RedeemEditShippingScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + @Test + fun screenShotTest() { + val localRedeemScreenPreviews = RedeemEditShippingPreviewParameter().values.toList() + localRedeemScreenPreviews.forEachIndexed { index, redeemEditShippingPreview -> + paparazzi.snapshot("parameter_$index ${redeemEditShippingPreview.name}") { + RedeemEditShippingScreenPreview(redeemEditShippingPreview) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/mocks/RedeemMocks.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/mocks/RedeemMocks.kt new file mode 100644 index 00000000..cd6b5a1e --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/mocks/RedeemMocks.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.mocks + +object RedeemMocks { + + const val testCertificate = """ + MIIFUTCCBDmgAwIBAgIDQNF0MA0GCSqGSIb3DQEBCwUAMIGJMQswCQYDVQQGEwJE + RTEVMBMGA1UECgwMRC1UUlVTVCBHbWJIMUgwRgYDVQQLDD9JbnN0aXR1dGlvbiBk + ZXMgR2VzdW5kaGVpdHN3ZXNlbnMtQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0 + dXIxGTAXBgNVBAMMEEQtVHJ1c3QuU01DQi1DQTMwHhcNMjExMDExMDM0ODU0WhcN + MjYwODE1MDcyOTMxWjBmMQswCQYDVQQGEwJERTEgMB4GA1UECgwXQmV0cmllYnNz + dMOkdHRlIGdlbWF0aWsxIDAeBgNVBAUTFzEwLjgwMjc2MDAzMTExMDAwMDAwNTQy + MRMwEQYDVQQDDApnZW1hdGlrMDA2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAmtDDCfvOJL82smWeqCKa1azV3SpMHOhO2P+ot6Yi+DRqANl/0HyUO+b5 + VGatK1ugqONe9f0jfwUCPKxr33V5dmtJ4F4Ywbjv5rfYhMdTR1XMbrzoOwAFhdve + 0k42dXbW2NCr8TZLz7xlcKihRphuzGbnGa+XpJriaw7g6fNmdo27Ad4tNIpezqFQ + WduRJMDnW+89bzOdicLmyKU2k6IK9Wpd8+TjQLtoG32IAxX/+auqf9wYZW9H7mGF + BagPxLO7D8cWaaX0K3JtRfCCE2hS7iBd6EqGCeoGz9NFg6aMDLxSOTuEgriTOI/O + WSXVpFyAp9amm6KUmdhKegQ0iSvS0wIDAQABo4IB4jCCAd4wHwYDVR0jBBgwFoAU + xk6YSKNeL3M/yJih5vVHqDXIhTowcgYFKyQIAwMEaTBnpCYwJDELMAkGA1UEBhMC + REUxFTATBgNVBAoMDGdlbWF0aWsgR21iSDA9MDswOTA3MBkMF0JldHJpZWJzc3TD + pHR0ZSBnZW1hdGlrMAkGByqCFABMBDoTDzktMi41OC4wMDAwMDA0MDBEBggrBgEF + BQcBAQQ4MDYwNAYIKwYBBQUHMAGGKGh0dHA6Ly9ELVRSVVNULVNNQ0ItQ0EzLm9j + c3AuZC10cnVzdC5uZXQwUQYDVR0gBEowSDA7BggqghQATASBIzAvMC0GCCsGAQUF + BwIBFiFodHRwOi8vd3d3LmdlbWF0aWsuZGUvZ28vcG9saWNpZXMwCQYHKoIUAEwE + TDBxBgNVHR8EajBoMGagZKBihmBsZGFwOi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0 + L0NOPUQtVHJ1c3QuU01DQi1DQTMsTz1ELVRSVVNUJTIwR21iSCxDPURFP2NlcnRp + ZmljYXRlcmV2b2NhdGlvbmxpc3QwHQYDVR0OBBYEFO4u6BXelEMIzPzPE3Dr+mYU + Eto/MA4GA1UdDwEB/wQEAwIEMDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUA + A4IBAQDVUgAkYpXjjeUJbj2fWEXcgiFC0xEk0yAwmY9jK6An0fT+cRC/quTdZx81 + BR0qt77ROBJ3Sw5CH5+Ai4bjfIsmPOtIFV3qlYWgkldXhUfNHO+pLtdSnlhr7q4M + pAoX8pyHrLyMPubJwBSeEHoY6yrW8bm1Pmo3NY/haOGEtuu6oS4hOqUD7kGHFsVp + xYQY3gSzVzSv8B2d/pQ6zt6PU2nAYPV+JmRGBXGKPL8ncvZuQK0UsuMpNW0Q7sP6 + YDxLibjz3631dSjPs5MxIinKVxRPPSm357w8ekTs89oWshDGMuY8Oz7pu4taFHpE + 3xlzYXhnic0Bj61g6O9YFjcL43No +""" +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsControllerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsControllerTest.kt new file mode 100644 index 00000000..e2506abd --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/redeem/presentation/RedeemPrescriptionsControllerTest.kt @@ -0,0 +1,499 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.presentation + +import app.cash.turbine.test +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.HTTP_INTERNAL_ERROR +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.messages.repository.CommunicationRepository +import de.gematik.ti.erp.app.mocks.profile.model.MODEL_PROFILE +import de.gematik.ti.erp.app.pharmacy.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.model.PrescriptionRedeemArguments +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.redeem.mocks.RedeemMocks.testCertificate +import de.gematik.ti.erp.app.redeem.model.RedeemedPrescriptionState +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnDirectUseCase +import de.gematik.ti.erp.app.redeem.usecase.RedeemPrescriptionsOnLoggedInUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.Instant +import kotlinx.serialization.json.JsonPrimitive +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Before +import org.junit.Test +import retrofit2.Response +import java.net.HttpURLConnection.HTTP_BAD_REQUEST +import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT +import java.net.HttpURLConnection.HTTP_CONFLICT +import java.net.HttpURLConnection.HTTP_GONE +import java.net.HttpURLConnection.HTTP_UNAUTHORIZED +import java.util.UUID +import kotlin.test.assertEquals + +class RedeemPrescriptionsControllerTest { + + private val prescriptionRepository: PrescriptionRepository = mockk() + private val communicationRepository: CommunicationRepository = mockk() + private val pharmacyRepository: PharmacyRepository = mockk() + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + private lateinit var redeemPrescriptionsOnLoggedInUseCase: RedeemPrescriptionsOnLoggedInUseCase + private lateinit var redeemPrescriptionsOnDirectUseCase: RedeemPrescriptionsOnDirectUseCase + + private lateinit var controllerUnderTest: RedeemPrescriptionsController + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(dispatcher) + + coEvery { pharmacyRepository.markPharmacyAsOftenUsed(any()) } returns Unit + + redeemPrescriptionsOnLoggedInUseCase = spyk(RedeemPrescriptionsOnLoggedInUseCase(prescriptionRepository, pharmacyRepository, dispatcher)) + redeemPrescriptionsOnDirectUseCase = spyk(RedeemPrescriptionsOnDirectUseCase(communicationRepository, pharmacyRepository, dispatcher)) + controllerUnderTest = RedeemPrescriptionsController(redeemPrescriptionsOnLoggedInUseCase, redeemPrescriptionsOnDirectUseCase) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `success - direct redemption`() { + coEvery { pharmacyRepository.searchBinaryCerts(any()) } returns Result.success(listOf(testCertificate)) + + coEvery { + pharmacyRepository.redeemPrescriptionDirectly( + url = any(), + message = any(), + pharmacyTelematikId = telematikId, + transactionId = orderId.toString() + ) + } returns Result.success(Unit) + + coEvery { + communicationRepository.saveLocalCommunication( + taskId = taskId, + pharmacyId = pharmacyId, + transactionId = orderId.toString() + ) + } returns Unit + + testScope.runTest { + advanceUntilIdle() + + controllerUnderTest.processPrescriptionRedemptions(directRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + // state after processing + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Success) + assertEquals(orderId.toString(), emittedState.orderId) + + cancelAndIgnoreRemainingEvents() + } + + coVerify(exactly = 1) { + redeemPrescriptionsOnDirectUseCase.invoke( + orderId = directRedeemArguments.orderId, + redeemOption = directRedeemArguments.redeemOption, + prescriptionOrderInfos = directRedeemArguments.prescriptionOrderInfos, + contact = directRedeemArguments.contact, + pharmacy = directRedeemArguments.pharmacy, + onProcessStart = any(), + onProcessEnd = any() + ) + } + + coVerify(exactly = 0) { + redeemPrescriptionsOnLoggedInUseCase.invoke( + profileId = any(), + orderId = any(), + redeemOption = any(), + prescriptionOrderInfos = any(), + contact = any(), + pharmacy = any(), + onProcessStart = any(), + onProcessEnd = any() + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `success - logged-in redemption`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.success(JsonPrimitive("jsonElement")) // the json element is not the right structure, but it is not relevant for the test + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + // state after processing + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Success) + assertEquals(orderId.toString(), emittedState.orderId) + + cancelAndIgnoreRemainingEvents() + } + + coVerify(exactly = 0) { + redeemPrescriptionsOnDirectUseCase.invoke( + orderId = any(), + redeemOption = any(), + prescriptionOrderInfos = any(), + contact = any(), + pharmacy = any(), + onProcessStart = any(), + onProcessEnd = any() + ) + } + + coVerify(exactly = 1) { + redeemPrescriptionsOnLoggedInUseCase.invoke( + profileId = loggedInRedeemArguments.profile.id, + orderId = loggedInRedeemArguments.orderId, + redeemOption = loggedInRedeemArguments.redeemOption, + prescriptionOrderInfos = loggedInRedeemArguments.prescriptionOrderInfos, + contact = loggedInRedeemArguments.contact, + pharmacy = loggedInRedeemArguments.pharmacy, + onProcessStart = any(), + onProcessEnd = any() + ) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_BAD_REQUEST`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_BAD_REQUEST, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_UNAUTHORIZED`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_UNAUTHORIZED, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_CLIENT_TIMEOUT`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_CLIENT_TIMEOUT, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_CONFLICT`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_CONFLICT, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_GONE`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_GONE, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `failure - logged in redemption HTTP_INTERNAL_ERROR`() { + coEvery { + prescriptionRepository.redeemPrescription( + profileId = loggedInRedeemArguments.profile.id, + communication = any(), + accessCode = any() + ) + } returns Result.failure( + ApiCallException( + message = "Error executing safe api call", + response = Response.error(HTTP_INTERNAL_ERROR, "Error executing safe api call".toResponseBody(null)) + ) + ) + + testScope.runTest { + advanceUntilIdle() + controllerUnderTest.processPrescriptionRedemptions(loggedInRedeemArguments) + + controllerUnderTest.redeemedState.test { + // initial state + assertEquals(RedeemedPrescriptionState.Init, awaitItem()) + + val emittedState = awaitItem() + assert(emittedState is RedeemedPrescriptionState.OrderCompleted) + + // results within the state + val prescriptionResultState = (emittedState as RedeemedPrescriptionState.OrderCompleted).results.values.first() + assert(prescriptionResultState is RedeemedPrescriptionState.Error) + cancelAndIgnoreRemainingEvents() + } + } + } + + companion object { + private val orderId = UUID.randomUUID() + + private const val telematikId = "9-2.58.00000040" + private const val taskId = "task-id-1" + private const val pharmacyId = "pharmacy-id-1" + private val prescriptionsForOrders = listOf( + PharmacyUseCaseData.PrescriptionOrder( + taskId = taskId, + accessCode = "access-code-1", + title = "title-1", + isSelfPayerPrescription = false, + index = 1, + timestamp = Instant.parse("2024-08-01T10:00:00Z"), + substitutionsAllowed = false, + isScanned = false + ) + ) + private val pharmacy = PharmacyUseCaseData.Pharmacy( + id = pharmacyId, + name = "pharmacy-name", + address = "pharmacy-address", + coordinates = null, + distance = null, + contacts = PharmacyContacts( + phone = "pharmacy-phone", + mail = "pharmacy-mail", + url = "pharmacy-url", + pickUpUrl = "pharmacy-pickup-url", + deliveryUrl = "pharmacy-delivery-url", + onlineServiceUrl = "pharmacy-online-service-url" + ), + provides = emptyList(), + openingHours = null, + telematikId = telematikId + ) + private val contact = PharmacyUseCaseData.ShippingContact( + name = "contact-name", + line1 = "contact-line1", + line2 = "contact-line2", + postalCode = "contact-postal-code", + city = "contact-city", + telephoneNumber = "contact-telephone-number", + mail = "contact-mail", + deliveryInformation = "contact-delivery-information" + ) + + private val directRedeemArguments = PrescriptionRedeemArguments.DirectRedemptionArguments( + orderId = orderId, + prescriptionOrderInfos = prescriptionsForOrders, + redeemOption = PharmacyScreenData.OrderOption.PickupService, + pharmacy = pharmacy, + contact = contact + ) + + private val loggedInRedeemArguments = PrescriptionRedeemArguments.LoggedInUserRedemptionArguments( + profile = MODEL_PROFILE, + orderId = orderId, + prescriptionOrderInfos = prescriptionsForOrders, + redeemOption = PharmacyScreenData.OrderOption.PickupService, + pharmacy = pharmacy, + contact = contact + ) + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseAccessibilityTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseAccessibilityTest.kt new file mode 100644 index 00000000..4f1d65f1 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseAccessibilityTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import androidx.compose.runtime.Composable +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import com.android.ide.common.rendering.api.SessionParams +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +abstract class BaseAccessibilityTest( + config: ScreenshotConfig, + renderingMode: SessionParams.RenderingMode = SessionParams.RenderingMode.NORMAL +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): List = ScreenShotConfigAccessibility.entries + } + + @get: Rule + val paparazzi = Paparazzi( + deviceConfig = config.deviceConfig, + theme = config.theme, + validateAccessibility = false, + maxPercentDifference = 0.2, + renderingMode = renderingMode, + renderExtensions = setOf(AccessibilityRenderExtension()) + ) + + fun Paparazzi.accessibilitySnapshot( + name: String? = null, + composable: @Composable () -> Unit + ) { + this.snapshot("accessibility-$name") { composable() } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseComponentScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseComponentScreenshotTest.kt new file mode 100644 index 00000000..e5dd4f27 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseComponentScreenshotTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +abstract class BaseComponentScreenshotTest( + config: ScreenshotConfig, + renderingMode: SessionParams.RenderingMode = SessionParams.RenderingMode.NORMAL +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): List = ScreenShotConfigComponent.entries + } + + @get: Rule + val paparazzi = Paparazzi( + deviceConfig = config.deviceConfig, + theme = config.theme, + validateAccessibility = true, + maxPercentDifference = 0.2, + renderingMode = renderingMode + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseScreenshotTest.kt new file mode 100644 index 00000000..e419d6b3 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/BaseScreenshotTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +abstract class BaseScreenshotTest( + config: ScreenshotConfig, + renderingMode: SessionParams.RenderingMode = SessionParams.RenderingMode.NORMAL +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): List = ScreenshotConfigScreen.entries + } + + @get: Rule + val paparazzi = Paparazzi( + deviceConfig = config.deviceConfig, + theme = config.theme, + validateAccessibility = true, + maxPercentDifference = 0.2, + renderingMode = renderingMode + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ReducedScreenshotTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ReducedScreenshotTest.kt new file mode 100644 index 00000000..a30571df --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ReducedScreenshotTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +abstract class ReducedScreenshotTest( + config: ScreenshotConfig, + renderingMode: SessionParams.RenderingMode = SessionParams.RenderingMode.NORMAL +) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): List = ScreenshotConfigScreenReduced.entries + } + + @get: Rule + val paparazzi = Paparazzi( + deviceConfig = config.deviceConfig, + theme = config.theme, + validateAccessibility = true, + maxPercentDifference = 0.2, + renderingMode = renderingMode + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigAccessibility.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigAccessibility.kt new file mode 100644 index 00000000..b82a9fd3 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigAccessibility.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.DeviceConfig +import com.android.resources.NightMode +import java.util.Locale + +enum class ScreenShotConfigAccessibility( + override val deviceConfig: DeviceConfig, + override val theme: String = "Theme.ERezApp" +) : ScreenshotConfig { + // German + LARGE_SCREEN_LIGHT_GERMAN( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // English + LARGE_SCREEN_LIGHT_ENGLISH( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.ENGLISH.toLanguageTag() + ) + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigComponent.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigComponent.kt new file mode 100644 index 00000000..54cc945c --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenShotConfigComponent.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.DeviceConfig +import com.android.resources.NightMode +import java.util.Locale + +enum class ScreenShotConfigComponent( + override val deviceConfig: DeviceConfig, + override val theme: String = "Theme.ERezApp" +) : ScreenshotConfig { + // German + LIGHT_GERMAN( + deviceConfig = DeviceConfig( + screenHeight = 600, + screenWidth = 768, + xdpi = 320, + ydpi = 320, + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + DARK_GERMAN( + deviceConfig = DeviceConfig( + screenHeight = 600, + screenWidth = 768, + xdpi = 320, + ydpi = 320, + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // English + LIGHT_ENGLISH( + deviceConfig = DeviceConfig( + screenHeight = 600, + screenWidth = 768, + xdpi = 320, + ydpi = 320, + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + DARK_ENGLISH( + deviceConfig = DeviceConfig( + screenHeight = 600, + screenWidth = 768, + xdpi = 320, + ydpi = 320, + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ); +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfig.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfig.kt new file mode 100644 index 00000000..4922b75a --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfig.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.DeviceConfig + +interface ScreenshotConfig { + val deviceConfig: DeviceConfig + val theme: String +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreen.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreen.kt new file mode 100644 index 00000000..15976c6f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreen.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.DeviceConfig +import com.android.resources.NightMode +import java.util.Locale + +enum class ScreenshotConfigScreen( + override val deviceConfig: DeviceConfig, + override val theme: String = "Theme.ERezApp" +) : ScreenshotConfig { + // German + LARGE_SCREEN_LIGHT_GERMAN( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + SMALL_SCREEN_LIGHT_GERMAN( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + LARGE_SCREEN_DARK_GERMAN( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + SMALL_SCREEN_DARK_GERMAN( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // German with big font + LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + LARGE_SCREEN_DARK_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + SMALL_SCREEN_DARK_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // English + LARGE_SCREEN_LIGHT_ENGLISH( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.ENGLISH.toLanguageTag() + ) + ), + LARGE_SCREEN_DARK_ENGLISH( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.ENGLISH.toLanguageTag() + ) + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreenReduced.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreenReduced.kt new file mode 100644 index 00000000..5d96b2e0 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/screenshot/ScreenshotConfigScreenReduced.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.screenshot + +import app.cash.paparazzi.DeviceConfig +import com.android.resources.NightMode +import java.util.Locale + +enum class ScreenshotConfigScreenReduced( + override val deviceConfig: DeviceConfig, + override val theme: String = "Theme.ERezApp" +) : ScreenshotConfig { + // German + LARGE_SCREEN_LIGHT_GERMAN( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + LARGE_SCREEN_DARK_GERMAN( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // German with big font + SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + SMALL_SCREEN_DARK_GERMAN_BIG_FONT( + deviceConfig = DeviceConfig.PIXEL_4.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + fontScale = 2f, + locale = Locale.GERMAN.toLanguageTag() + ) + ), + + // English + LARGE_SCREEN_LIGHT_ENGLISH( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NOTNIGHT, + locale = Locale.ENGLISH.toLanguageTag() + ) + ), + LARGE_SCREEN_DARK_ENGLISH( + deviceConfig = DeviceConfig.PIXEL_6_PRO.copy( + softButtons = false, + nightMode = NightMode.NIGHT, + locale = Locale.ENGLISH.toLanguageTag() + ) + ) +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/AdditionalLicensesScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/AdditionalLicensesScreenTest.kt new file mode 100644 index 00000000..a3aed947 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/AdditionalLicensesScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.screens.AdditionalLicensesScreenScaffoldScreenPreview +import org.junit.Test + +class AdditionalLicensesScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + AdditionalLicensesScreenScaffoldScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreenTest.kt new file mode 100644 index 00000000..6d3bbc75 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAllowAnalyticsScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.screens.SettingsAllowAnalyticsScaffoldContentPreview +import org.junit.Test + +class SettingsAllowAnalyticsScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + SettingsAllowAnalyticsScaffoldContentPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAppSecurityScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAppSecurityScreenTest.kt new file mode 100644 index 00000000..fd197918 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsAppSecurityScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.preview.AppSecuritySettingsParameterProvider +import de.gematik.ti.erp.app.settings.ui.screens.SettingsAppSecurityScreenScaffoldPreview +import org.junit.Test + +class SettingsAppSecurityScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = AppSecuritySettingsParameterProvider().values.toList() + parameters.forEach { previewData -> + paparazzi.snapshot(previewData.name) { + SettingsAppSecurityScreenScaffoldPreview(previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLanguageScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLanguageScreenTest.kt new file mode 100644 index 00000000..e16de56d --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLanguageScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.screens.SettingsLanguageScreenScaffoldPreview +import de.gematik.ti.erp.app.utils.compose.preview.LanguageCodePreviewParameterProvider +import org.junit.Test + +class SettingsLanguageScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = LanguageCodePreviewParameterProvider().values.toList() + parameters.forEachIndexed { index, languageCode -> + paparazzi.snapshot("parameter_$index") { + SettingsLanguageScreenScaffoldPreview(languageCode) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreenTest.kt new file mode 100644 index 00000000..c4131a67 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsLegalNoticeScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.screens.LegalNoticeScreenScaffoldScreenPreview +import org.junit.Test + +class SettingsLegalNoticeScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + LegalNoticeScreenScaffoldScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreenTest.kt new file mode 100644 index 00000000..a63c5244 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsOpenSourceLicencesScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.screens.OpenSourceLicensesScreenScaffoldScreenPreview +import org.junit.Test + +class SettingsOpenSourceLicencesScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + OpenSourceLicensesScreenScaffoldScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreenTest.kt new file mode 100644 index 00000000..fd6f96ec --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsProductImprovementsScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.preview.SettingsProductImprovementPreviewParameter +import de.gematik.ti.erp.app.settings.ui.screens.PreviewSettingsProductImprovementsScreen +import org.junit.Test + +class SettingsProductImprovementsScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = SettingsProductImprovementPreviewParameter().values.toList() + parameters.forEach { analyticsAllowPreviewData -> + paparazzi.snapshot(analyticsAllowPreviewData.name) { + PreviewSettingsProductImprovementsScreen(analyticsAllowPreviewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreenTest.kt new file mode 100644 index 00000000..ab447633 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.preview.SettingsScreenPreviewProvider +import de.gematik.ti.erp.app.settings.ui.screens.SettingsScreenPreview +import org.junit.Test + +class SettingsScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = SettingsScreenPreviewProvider().values.toList() + parameters.forEachIndexed { index, settingsProfile -> + paparazzi.snapshot("settingsProfile.name_$index") { + SettingsScreenPreview(settingsProfile) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreenTest.kt new file mode 100644 index 00000000..178a661b --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsSetAppPasswordScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.settings.ui.preview.SetAppPasswordParameterProvider +import de.gematik.ti.erp.app.settings.ui.screens.SettingsSetAppPasswordScreenPreview +import org.junit.Test + +class SettingsSetAppPasswordScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = SetAppPasswordParameterProvider().values.toList() + parameters.forEach { previewData -> + paparazzi.snapshot(previewData.name) { + SettingsSetAppPasswordScreenPreview(previewData = previewData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreenTest.kt new file mode 100644 index 00000000..b16e92d8 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingDeviceOnTopScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingScaffoldPreview +import org.junit.Test + +class TroubleShootingDeviceOnTopScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + TroubleShootingScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreenTest.kt new file mode 100644 index 00000000..dbd55d7e --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingFindNfcPositionScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingFindNfcPositionScaffoldPreview +import org.junit.Test + +class TroubleShootingFindNfcPositionScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + TroubleShootingFindNfcPositionScaffoldPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreenTest.kt new file mode 100644 index 00000000..3835f629 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingIntroScreenTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingIntroScreenPreview +import org.junit.Test + +class TroubleShootingIntroScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot { + TroubleShootingIntroScreenPreview() + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreenTest.kt new file mode 100644 index 00000000..bb514808 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/troubleshooting/ui/TroubleShootingNoSuccessScreenTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.troubleshooting.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.troubleshooting.ui.preview.TroubleShootingScreenPreviewProvider +import de.gematik.ti.erp.app.troubleshooting.ui.screens.TroubleShootingNoSuccessScreenPreview +import org.junit.Test + +class TroubleShootingNoSuccessScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = TroubleShootingScreenPreviewProvider().values.toList() + parameters.forEach { troubleShootingScreenData -> + paparazzi.snapshot(troubleShootingScreenData.name) { + TroubleShootingNoSuccessScreenPreview(troubleShootingScreenData) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/userauthentication/UserAuthenticationScreenTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/userauthentication/UserAuthenticationScreenTest.kt new file mode 100644 index 00000000..c36db117 --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/userauthentication/UserAuthenticationScreenTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.userauthentication + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.userauthentication.ui.preview.UserAuthenticationPreviewParameterProvider +import de.gematik.ti.erp.app.userauthentication.ui.screens.UserAuthenticationScreenPreview +import org.junit.Test + +class UserAuthenticationScreenTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + val parameters = UserAuthenticationPreviewParameterProvider().values.toList() + parameters.forEach { previewData -> + paparazzi.snapshot(previewData.name) { + UserAuthenticationScreenPreview( + previewData = previewData + ) + } + } + } +} diff --git a/app/features/src/test/kotlin/de/gematik/ti/erp/app/utils/banner/ui/NoInternetBannerTest.kt b/app/features/src/test/kotlin/de/gematik/ti/erp/app/utils/banner/ui/NoInternetBannerTest.kt new file mode 100644 index 00000000..c1581e9f --- /dev/null +++ b/app/features/src/test/kotlin/de/gematik/ti/erp/app/utils/banner/ui/NoInternetBannerTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.banner.ui + +import de.gematik.ti.erp.app.screenshot.BaseScreenshotTest +import de.gematik.ti.erp.app.screenshot.ScreenshotConfig +import de.gematik.ti.erp.app.utils.compose.SimpleBannerPreview +import org.junit.Test + +class NoInternetBannerTest(config: ScreenshotConfig) : BaseScreenshotTest(config) { + + @Test + fun screenShotTest() { + paparazzi.snapshot("NoInternetBanner") { + SimpleBannerPreview() + } + } +} diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..41ec3fbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:008a6d3900afa150e6d322583545fba16bbd267c817b5baaff9a6ea2ff6da230 +size 86180 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..aeffdd84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8833a7bed9933a6f5bf18270871ff47d301ad71966a3a28d3b9528fc8bdcd116 +size 93001 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d7784be7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52306cb9ebb6b40f07b3dea7ab1f4356dfc025e615dedead18a89ae8b663ce3a +size 87841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..7c6a7354 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b559d9871aa54ef5ffae65b57f5550fb0b4cb88cb5c986bb34d222acfc0a190a +size 84580 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..1935297d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f15423342376780bcd881b0ed2b313b2f3142232a4d425832c726031392edd20 +size 90884 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0498ad68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bc2c50ed52c87d1bd6b7dcd924ff76f5b26f36fcbcc7cda34821430fcd6ee39 +size 86709 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..26b13f49 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4bda50a9639c2fa3cd6eb77d9e20e97061a249c862263d351122fbed8f2fc53 +size 111055 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..42e12d2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2024ff16fefae002f03396bdddbc327bebc9127f047fe13bc174d475231facf4 +size 81271 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..706b7f32 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83749e78bde423597d2a6c08bed1c8835bf9dbf1901e414395a1d90de6b08c96 +size 107778 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c925d96b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.analytics.ui_OnboardingAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a511744227cc3d61a5ec654c646622e0f0db1cd2f9d7f838ec8e3505253fce +size 79951 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png new file mode 100644 index 00000000..38d3f9f4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4446ef566e917e3c9fd7b2da243a3da667512581d4b1ac2d96b157772a2bec74 +size 84055 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png new file mode 100644 index 00000000..bd76ad89 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7811be1fa5de6c0b5a454d22b1a78ec8a2282d105e1b1a78d602a180559cde85 +size 84177 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png new file mode 100644 index 00000000..be223fe2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fbcf2d2f7962d9837e9346b3dd2a6655b0cac364af4d4c3f00dcf65f191bc3f +size 87141 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png new file mode 100644 index 00000000..50bde6d4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab7347c0cd1a9938da36f38e166edaf7c37af814986bc772bc15a05292e9a67a +size 87286 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..cac1c4bc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3b393429420cc4518b8de0f1121a13d8219c1193ba67574eb80a442c4518439 +size 109727 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..ea84f1bd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ee02dec83e3475fb07566243951588cb2599cdc0cff9f9040906051d85df03f +size 109754 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png new file mode 100644 index 00000000..c6626397 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d4c6c103b09c94c65d66b65b03385762978014f989965b3e429d8f187485f31 +size 78708 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png new file mode 100644 index 00000000..9619f6a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b017256547d91aab7b2bd0c3799fefdab6b05fab1b98f674761322b6fa90aa9 +size 79077 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png new file mode 100644 index 00000000..0a0e1344 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5db9392562627515818c41d7d8d9ae34c3060abca1e402787e4e0db5e95c0f5 +size 81826 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png new file mode 100644 index 00000000..8778dfca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98ad77c424c1fc3f631abd4f828d5cf355c14ddda2229267e4373acfe3755877 +size 82179 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..44f28d62 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0578a7e619048002b6a4328c0d2e8ac004335484f76dbf84e434f7b55cf85313 +size 103581 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..120799eb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05df5457a2153620f293b793e18b499c05fe603816cd6c1f32b98785e9608f28 +size 103715 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png new file mode 100644 index 00000000..7e0464d0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19bc8fda2140755fadf725a0d9322f30524e93b61b38c9ab446ccb6eb0521884 +size 94194 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png new file mode 100644 index 00000000..cd738656 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f744740581399028cb1d1d1e053224a48a0f4853ef4b04c075125febf91eccda +size 94367 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..bd4469f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9d5166acb21c1f7d17941f794f23770f0d2865e1af1c348bb4105347d22109b +size 114655 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..bd4469f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9d5166acb21c1f7d17941f794f23770f0d2865e1af1c348bb4105347d22109b +size 114655 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png new file mode 100644 index 00000000..39bf553d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97800e6090a36f326dd335634366fb17e08252932b4d9d30d763b07411f58da0 +size 88364 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png new file mode 100644 index 00000000..d8194626 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d9909e62265ac29686b5ef8cb2f752db72e709ccef6c0d57866edc46c76b9d +size 88790 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..299d4445 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe7f1af486f08aa163eca052aacc2076766c49862d3939705e671f5efd7cc33 +size 107590 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..299d4445 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_InsecureDeviceScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe7f1af486f08aa163eca052aacc2076766c49862d3939705e671f5efd7cc33 +size 107590 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png new file mode 100644 index 00000000..b04e7b21 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:498733bf3a0c6b930172172226d6e0fb2eb24bba984260dce369ca6671134718 +size 93629 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png new file mode 100644 index 00000000..a6dc4ed5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ee1b47a01bd9a35a2ecc590266c7dd14aca48a95e2e1547b9cd6d324c4cc8ab +size 93729 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png new file mode 100644 index 00000000..eb4c2f85 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e981aaa8d4f588288633bdad58cae057b7991634acc371896c66381610c75539 +size 97348 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png new file mode 100644 index 00000000..864d085d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d503e3cad0011e1296d001a3d04b79c946349435bdaae246e27e73541ace3d2c +size 97604 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..49b873f5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:329aca6c2f2753f9e4ce56b6636f3dd9e0da682d627812bd74bb57e41fa32a9d +size 115589 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..49b873f5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:329aca6c2f2753f9e4ce56b6636f3dd9e0da682d627812bd74bb57e41fa32a9d +size 115589 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png new file mode 100644 index 00000000..8c9a4094 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5b74b4b2a3e6c58b61dedb74e706b04e23e9fe962bc8cb98cc88bcaa0f0670e +size 84540 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png new file mode 100644 index 00000000..be4df693 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fc265bf0b9cb295e89d615c16474484f6ce1e4dbf4f74832354fc61da74ec23 +size 84857 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png new file mode 100644 index 00000000..c07c13c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83e66ef0c6ff8f956da2b7e89c4663ea9b94337eceed685e0a6c745b5a739e0e +size 88712 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png new file mode 100644 index 00000000..386e6885 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99023aa589ba0bf829e1a8272d9f07a7edf4dc0348a29e4a5ccb76b93b5aeba4 +size 89048 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..932a38e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df82a7e895e9a72edb11b4a77dffb691f4ccbc2a99461fadbb8dccffca3c2929 +size 104680 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..932a38e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df82a7e895e9a72edb11b4a77dffb691f4ccbc2a99461fadbb8dccffca3c2929 +size 104680 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png new file mode 100644 index 00000000..2d79850d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71efd6f0d9d40846d16f3f89fdeaaf96cc4811cde447a3d9cb80974fb0f17ad4 +size 107166 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png new file mode 100644 index 00000000..56ed27a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73c11afddd2e2bc709e4b9c38971d0cb38ff60bba7d11aba002e12840bef6311 +size 107350 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..52da5d71 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ff9682e8633e3da4f5f58edd21302795dd2ba1a19c3a8dc595c8e4696665f61 +size 116271 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..52da5d71 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ff9682e8633e3da4f5f58edd21302795dd2ba1a19c3a8dc595c8e4696665f61 +size 116271 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png new file mode 100644 index 00000000..326d6038 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b038df5af15665bb0f90e401aabc077ffdb71b63fb25a1d72fa8eabd8ace0b5 +size 97007 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png new file mode 100644 index 00000000..764e27dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7151bcc28ea93c9dd26e42c8906a5368fcfbfd5b539973153c0262b7749e5aad +size 97289 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png new file mode 100644 index 00000000..7fa8fdd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1d8d556d2e86b8920442a2c0b3b1677c0bb345ce4beda4edac012609c52c824 +size 105302 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png new file mode 100644 index 00000000..7fa8fdd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.appsecurity.ui_IntegrityWarningScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_checked_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1d8d556d2e86b8920442a2c0b3b1677c0bb345ce4beda4edac012609c52c824 +size 105302 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..f3ea47bc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21c168178300f0e9f0a73013e55d05a9015558b6019ef3aca0bb21274c06cfba +size 147665 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..3fc77be0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d340b8479d29c744611528cc3fbab9a00406de620419ffa394cc3f063415f89e +size 148174 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..fcebfb8d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3361ca05da9cb92b24c6f98b07769d2acccf733de460484d2d150ef32080f2ef +size 146856 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..202f9e75 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1b901866064dcff6ecd2401faf1e8b9627bfe78d175b6177aa080829464c6b6 +size 147375 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..589a00c0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0564a7063c06e92a3f94afc2b6307ee996a3be1da7f5ae77e41b016f9f85450 +size 149687 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..6c850506 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f54e44506eb520cf21bcef96eaaf608d1c9660f7f49179896d839d5d21740eb +size 150345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..4188d931 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf433046289c5466293f41249e6182b4c1c57c0c8a4606a2f9a7b3bb95e9c84e +size 148991 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..8c17e728 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4b732c41fef474bbe14915bb2798141d5c3605a5fb7c8a6066aba2647513f39 +size 149653 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..78a209d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccbd1338c9edab5706a8470841167525c4b330c4810e4978d42d78aac2649967 +size 168556 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..1a0fad2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a3faf8cadee0f2b8085643aa3fdf551934f59755cfee8d1b598180910bb36a +size 169371 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..8ef359f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c264aef6eaba6d6a9a2295a0158463ea0aa73da2f728e1d0a3f04dda3a74ebbf +size 167137 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..067060ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b844acbe068d7c8ffe025c31d1c52fe5a6ca69928c89e6595af7d450fbcf14e0 +size 167975 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..028e8351 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e34aaae9f770aa775af389a675bd94e10f5f9d10e5690503739ae51e6ddf75c +size 146595 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..412e0fd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3fa93c86f26163c726113d8628a3240df3e31299489e1126b0dea6d1fa51c14 +size 146944 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..25da7166 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:774fb6ecda773023e2949bd21d94c379743e6a09f7e8e648eb34d361f8606cac +size 145913 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..722c04ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:551a1549a491c9b319aae1f1209d74455ce0a2e84bf678000c7d479d7fc20d9d +size 146250 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..47a9ed95 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa3d32ad200ac44d7c368624ca5bbe17fd3b9557cf70a1d6fbd021deedd55c02 +size 148240 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..1f27c3ce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f98cebf00eece66f4492057de741c2714f60c4a9f3bfd2e98bc5988c2028500 +size 148630 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..43060b89 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca940fa6db12dd6a8d6f0a5fba8094c0a1edfa044c6e40282f63791a0f0d4a0d +size 147614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..bcb42968 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b968cd8154c7a23408770c32b22b7515bdaa27dbe794a1879a1730fe020c997 +size 148019 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..d360dc1e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1cc92219b60ea725b7e69b099a2a9fd9c9f40c1b2bd497f8a36cb73527652c5 +size 166456 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..746f2a83 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dcf5c2812570a5d58d34cc12e3b139ecc74ed25306c5e9d55c3855ad282c69c +size 167129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..187b1025 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15bf9a00573ae6f8ccfeb11c4eb9cc49fd96a745f1fe15505e6026eba34eba6d +size 165187 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..24fc5ea2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05232a3d6125548f2fa04320e6ba64ee631f2df4d16800bfcbe0f6ebb4d973b1 +size 165900 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..1fe82db1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84924fc13a4a2757c19b0f2164d59e19fa73f1d06eb87c049c5bccbd747de1d2 +size 158040 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..4a948739 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:827698e948caecc26218f30831ad04b804b8a4bab69585d7bd5baa42cc5eeb96 +size 158849 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..0c55a39c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53ac465a89fc57d18a66f2ecd80d67908feeb9e7951044f1b0a83aab57bd1398 +size 157317 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..e8ab52d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8413bf59aff0c36eca65fa41a723277e3069970bc56fcd0c8a18d81281f9d2ed +size 158111 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..435cf1aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9615d918464bc09db72b6a5aff3fa71a6e928e17d9b460da7fb4b9d70147cb2 +size 175543 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..f06bc71e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac157a5face3446fd68e99c8ea2e23269668d7d1dbef50d5dbc591bc5b78f781 +size 176669 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..3b784052 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27193a4b006f327f3c13dbd5cba20f1ec75aaf1433d9c3d98ec73c2065e3927e +size 173699 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..8a4d5333 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bbf40e509a300d6f8bc0b7fa0cb51ab3d88c28337ef7fa887ab18ff4547acb1 +size 174823 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..eecd9f10 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:142a8ff5a17951c1b1610db53b7afc9d0370eb36177566f2003de64b7bca592c +size 156480 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..3036aeca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:682c366d2adb8059a0e1859364f1ac80a28b8a146214e90aca3810927f35a506 +size 157024 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..58ca4931 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f5649b8236849714182c5967bed99919668d773a1657327d06257a91d6e696f +size 156076 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..3bcc3adc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80872c3807d0b2d7d7fb1b7a3af8ae1b1f7f19060102a0512b0c7002c532dfb4 +size 156613 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..a7c50f05 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0600522e82c4dee1b0cc61ba3448a03c1dacb907369a96ceb8f954d9c16c8483 +size 174226 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..fccc307c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d549b19409fad6575fe65e3b03339e9465730c56c0b08d48fed484a73dec313 +size 175089 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..a837c913 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99717e3c89f93a67c220f38e789e9494d55938aa5949f355965afe4a0ee29a43 +size 172708 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..9561c103 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7adb9e9d9a4bce50b1335a29ab7aecfbc1021653ae8e0e2f6f0a8b2a72669fab +size 173564 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png new file mode 100644 index 00000000..69444dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:121ed1feecf8ba09284663bec807944a0be4adfdd715a8086b748fb395557b69 +size 23844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png new file mode 100644 index 00000000..fc8c612a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f7e9a73764804174d7b46b441c65a641730c33de766aacfe4379498e979eb27 +size 24100 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png new file mode 100644 index 00000000..a1b9433e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634cfdf2409bc207c46a2d32d30bf71274a4083370915db5714d7dbe2cebde95 +size 23978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png new file mode 100644 index 00000000..dca91216 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51412f0f404b24f6827aceca7c0d0d0035eb954a9e9ab4c3915442e81d0cb868 +size 22788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png new file mode 100644 index 00000000..b04a37fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d03f7856a947194fde4febd99ae8ef0b4eae368c51559158a3b9166530846c +size 23957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png new file mode 100644 index 00000000..a5e8d671 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfecb3b7c54215e6a69da10119a51eff7e9f6652e6a565c8f921215671b8ca92 +size 27924 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png new file mode 100644 index 00000000..e1459497 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c32e73d52392e9adbfd6681cf36c9ec3f3668b136dabd658e2d5b1449d298319 +size 28003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png new file mode 100644 index 00000000..07aa86ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc1961bb8706ec632aec40813dae67d144f30c6e30ff72d8a8ba111d5f0afc1c +size 27996 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png new file mode 100644 index 00000000..24af1ef7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e46e8d7cd5d5282052c7b1e17e864ad131421953aa2b7d4695ed722be4d5875 +size 26378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png new file mode 100644 index 00000000..be4271aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b24360a08c1db1399593d3739dde2c94ca88e53f751c54353908168126911a9d +size 28010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png new file mode 100644 index 00000000..69444dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:121ed1feecf8ba09284663bec807944a0be4adfdd715a8086b748fb395557b69 +size 23844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png new file mode 100644 index 00000000..fc8c612a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f7e9a73764804174d7b46b441c65a641730c33de766aacfe4379498e979eb27 +size 24100 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png new file mode 100644 index 00000000..a1b9433e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634cfdf2409bc207c46a2d32d30bf71274a4083370915db5714d7dbe2cebde95 +size 23978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png new file mode 100644 index 00000000..dca91216 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51412f0f404b24f6827aceca7c0d0d0035eb954a9e9ab4c3915442e81d0cb868 +size 22788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png new file mode 100644 index 00000000..b04a37fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d03f7856a947194fde4febd99ae8ef0b4eae368c51559158a3b9166530846c +size 23957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png new file mode 100644 index 00000000..a5e8d671 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfecb3b7c54215e6a69da10119a51eff7e9f6652e6a565c8f921215671b8ca92 +size 27924 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png new file mode 100644 index 00000000..e1459497 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c32e73d52392e9adbfd6681cf36c9ec3f3668b136dabd658e2d5b1449d298319 +size 28003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png new file mode 100644 index 00000000..07aa86ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc1961bb8706ec632aec40813dae67d144f30c6e30ff72d8a8ba111d5f0afc1c +size 27996 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png new file mode 100644 index 00000000..24af1ef7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e46e8d7cd5d5282052c7b1e17e864ad131421953aa2b7d4695ed722be4d5875 +size 26378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png new file mode 100644 index 00000000..be4271aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b24360a08c1db1399593d3739dde2c94ca88e53f751c54353908168126911a9d +size 28010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png new file mode 100644 index 00000000..120052d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:560a5ab6a035b70473ada37932c1e2680b82deb07182fb9b991c10cad6d30a4c +size 55413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png new file mode 100644 index 00000000..3114853f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96536552ae8a67e9d475c7e020b238750f60ebc8df59a1bb99847fd67747ece +size 55770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png new file mode 100644 index 00000000..b4a6e699 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3afda8cf9192f21614fc9da8083ef5cad2f82dd5799d6c97b6835ffbe132efd7 +size 55857 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png new file mode 100644 index 00000000..16608c4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68cc0f51573cbdd611b61e5a5caabef2f22c9b18ff12fdb5a32056bf2a52a3ed +size 52249 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png new file mode 100644 index 00000000..9341e299 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7abf232fbc1326a6fd07a477bde5615edbdec69e941ca4dab0ee470a4a80088c +size 55780 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png new file mode 100644 index 00000000..120052d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:560a5ab6a035b70473ada37932c1e2680b82deb07182fb9b991c10cad6d30a4c +size 55413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png new file mode 100644 index 00000000..3114853f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96536552ae8a67e9d475c7e020b238750f60ebc8df59a1bb99847fd67747ece +size 55770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png new file mode 100644 index 00000000..b4a6e699 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3afda8cf9192f21614fc9da8083ef5cad2f82dd5799d6c97b6835ffbe132efd7 +size 55857 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png new file mode 100644 index 00000000..16608c4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68cc0f51573cbdd611b61e5a5caabef2f22c9b18ff12fdb5a32056bf2a52a3ed +size 52249 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png new file mode 100644 index 00000000..9341e299 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockEgkScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7abf232fbc1326a6fd07a477bde5615edbdec69e941ca4dab0ee470a4a80088c +size 55780 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..61254992 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c659436ca83e1802fb5d6944d8ff025238fd967a2a86dfb2d3309b8002e9314 +size 28609 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_none.png new file mode 100644 index 00000000..607f4d91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7e07632c1f4caa81856fbadde0cc38a73edab9d8afb255b46d7e007ba55f5a4 +size 22464 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..607f4d91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7e07632c1f4caa81856fbadde0cc38a73edab9d8afb255b46d7e007ba55f5a4 +size 22464 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..3049523a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26b5ae95af36acbfdfd1e30a1c573dfc75a3e29bcaf6ae923e61108ca6ccde50 +size 22347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..a9afc3f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c0b811a298a801f0b1dc569e808a632339a4f5d6a2f90e16e5aeed3e5cabbf7 +size 31622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_none.png new file mode 100644 index 00000000..4cad837f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54cdbe321feb44b22ef76b2ba9b516c72d83e0c5fc0ee9698604713bc7c30462 +size 25478 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..4cad837f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54cdbe321feb44b22ef76b2ba9b516c72d83e0c5fc0ee9698604713bc7c30462 +size 25478 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..7148f2bc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfcef7bcaf16f722fcb9796cb96d63ad766e6367b9a5830edc211964b6657264 +size 25265 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..004a1478 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b707d6b2493ef4272ad00d13ad678f9c451f1fef6464febb0b25e624ad6e9712 +size 57293 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png new file mode 100644 index 00000000..cb1eb377 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c3f39dbba16f0f6e018a60c31269b9a41ee4b1e25ea12212d02ceb1b30f97a +size 42093 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..cb1eb377 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c3f39dbba16f0f6e018a60c31269b9a41ee4b1e25ea12212d02ceb1b30f97a +size 42093 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..8b735450 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb907e13ba17124bc90c4f5f75300b01361dccb4375e2b70fcf4c072dd665b59 +size 41686 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..3555845c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ece1e5ad7c653718a8abe0b86dae897397e0fb8f83cd8884966b6a7601d583b +size 27787 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_none.png new file mode 100644 index 00000000..421740cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b57d5d747c0e49cbd15d0110e468cae4b6f9d2d21d0e8f1cd72691d41abfca9 +size 21910 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..421740cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b57d5d747c0e49cbd15d0110e468cae4b6f9d2d21d0e8f1cd72691d41abfca9 +size 21910 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..709a6d40 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b50af54e1222d1ced567aaf3b83b455cb1838a12236e83d4003e1ce7477872ca +size 21833 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..0533567b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33e14a741fd0042fbf067945825aafa3b01d636ec7d3d30c21e546887fa8d4dc +size 29986 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png new file mode 100644 index 00000000..82f8d085 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5853b40c8a1fe488200aadd61626cdfe335ab324306e0fb8716d9cc50c619ad7 +size 24718 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..82f8d085 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5853b40c8a1fe488200aadd61626cdfe335ab324306e0fb8716d9cc50c619ad7 +size 24718 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..6ea2170e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a495b940b1d891f00bb69e24f1eb64216199a1498891aad6cfa6028d0b82bf3 +size 24562 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..d01b4e70 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6f1bdab21a0faef39b79d164644d2081187bd36ccecaa23b3fbfb565b011a18 +size 55003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png new file mode 100644 index 00000000..89faf7ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:301ba4324687e35851a41a9bd643b94a4f6f09567de00d77f3c8f39c8ee6b286 +size 40497 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..89faf7ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:301ba4324687e35851a41a9bd643b94a4f6f09567de00d77f3c8f39c8ee6b286 +size 40497 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..44876c7d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e80b95ce16912d8633fc7dd0594ab0b13964bfe7c77b535d07b07ec4a093a146 +size 40067 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..5d3debfe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b4a404d3e06607549afaa62dd3bb6e05280186a5232e6d3ab5e60cd49974d77 +size 36663 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_none.png new file mode 100644 index 00000000..68009196 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13623064bcff969d9c8c9e2defac708618c77e13d66a02f901771def7666e99d +size 28555 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..68009196 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13623064bcff969d9c8c9e2defac708618c77e13d66a02f901771def7666e99d +size 28555 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..6173e2ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0cf014b63594f155c746198ebd6f9771194e06b3c87d327790e56d4e21d31fd +size 28418 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..09441888 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9762e59a187c9ed846b51a66c3f763660aa0c18e6aebd879f24b95289231e64 +size 67834 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png new file mode 100644 index 00000000..782e1811 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:641bdcfa614129b5ad00b0538dbc771d28dc6f82343ccadfd38feb7d5bac8a17 +size 48584 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..782e1811 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:641bdcfa614129b5ad00b0538dbc771d28dc6f82343ccadfd38feb7d5bac8a17 +size 48584 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..84cf28df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f69e487a2055a0190036df3400e82f31229e994a6bba40a54904f845531e005e +size 48147 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..73605ae5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:497d89242f39b6782f95e3ff76e780a3402afcdb48c73196a4b248e5237d0a8a +size 34913 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png new file mode 100644 index 00000000..2fa88373 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029da150e4a7868a412e0f74f7c7034b48739abf1321a0b72f61992cba0d9e4e +size 27952 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..2fa88373 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029da150e4a7868a412e0f74f7c7034b48739abf1321a0b72f61992cba0d9e4e +size 27952 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..30e9aa32 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4f8805ec0de5e6287ce3b501c29ef3467bb7e850fc8c69a4c14095ee55b102d +size 27776 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png new file mode 100644 index 00000000..714fb487 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_changereferencedata.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa02509a2cfa75b06dc5b8416137a8ffb378eefa31c0705f7b947972b6168c2e +size 63869 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png new file mode 100644 index 00000000..a8b25341 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f7719355472457177f8033e587effa92c91dc4f1022b7748e8d0bf1e0e1f6e +size 46542 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png new file mode 100644 index 00000000..a8b25341 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f7719355472457177f8033e587effa92c91dc4f1022b7748e8d0bf1e0e1f6e +size 46542 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png new file mode 100644 index 00000000..d4a72cee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_unlockmethod_resetretrycounterwithnewsecret.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1ac9a3f031df5c315f58286996e15c66aff3a90d0def1c19bdaeddfb1e6203f +size 46096 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..6d6d2400 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ad89a88b6122d5f001f381e041e2f0622164be20d3bc342ac15718e30dfe4af +size 38308 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..00cf4777 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c1bbc491f49ad3eca54115b7e00e433859e4b61ba1c50f829dd0b646fabb502 +size 38293 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..0e56d4d8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45b4664d04beb392d0836919fc18afedc3509543c2b12a4153b6689297af9fde +size 41226 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..2bb4976c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70664ca36ecfe10393ed1c2496b32b6178e7b94f9d1e7211388f679a63713e32 +size 38420 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..06e6f4e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2c48b2f5394899b1afd74346bb679a94d1250f8f51e383f324c14077bb1ca0d +size 40911 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..e8972dcc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fea791cc2c11ec8c8a26c9cbba1adf4bc507fb55fa37efe533255232247ae9a +size 40765 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..4e3d6fe9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c890570948dd3f6d4f18f47f328008924f384ed028dd9bba21c912b5779e8f4c +size 44132 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..14a9e66c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:458b2fb33609e627397b357de703cb3bfb6f5b5746f9031afc964a1e4bd4c0ce +size 41043 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..3a1e4721 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c64b8e5048aa72fafcb966731910ae919053c8890f684c040423eb56dfdad155 +size 58910 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..1e839199 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbacf57363fa90a3466357bbc3c4ac8ef13b9696585dbdec3b3cb453d791f112 +size 59671 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..64d1d68e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55689394a122f815982844953cc62cf3e8038bc0f71ea4345113fa65442ebaf3 +size 66744 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..45d6659a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b34519881bab19843cbe6fb2f0cca77333663aceba85364d0661ae1ca2bc26bd +size 59341 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..b9897d66 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2f64f9aeb02dce4ff3ef1900182d37ac6cac04123ab39816be58ea76b4ec635 +size 35016 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..276f949e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d691678c738a855635fccffc081186e58859533886bff18e814b9002c746fff5 +size 35300 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..5bf30662 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:388601afc74ab5d575cc8bd3815cea358106c354cf08b6a3b7710b2e6d8d0905 +size 38156 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..dd368a3d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12aed5fd1cdb9688ec65fca8421b1fac41e8154048205b499a50f14e0bdcbbd1 +size 35124 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..0fa0886f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c9ec9b882b8139b5483a6ef1c3b56704c4b509871767371cea764e8233d8d5b +size 37472 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..26cd7ac6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0589ee1f58b2a679a3d7bb59c8d5c6b5bd65a721703aaf650f0da357a2d4f254 +size 37746 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..6b5fec42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d907c5b704399b341a0b8e8ebbc9eeb1f0560cf65c010e7853d6ee87aa059a5 +size 40958 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..e7cb3aba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e59a3fd2d3e80ec7ecf061187c99366a42a60ff8bfc5a0978cc98f0f23511ce1 +size 37584 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..f23c221a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37b681ef313d8e0fcef9d03d2754a6019023f05735528fd8c700d27f4036cb72 +size 54574 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..cec9c6c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e6ee158c67650731dc7168d39319bc34cb20ed334a9e178128ce343e3296c58 +size 55808 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..87f1b183 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2eb690233e895691f0c5b581e5cc099f53bd39a655946ac84dbd767294065f41 +size 62723 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..1477ba51 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:552dd8babc119309c7d7bb710bf0b5d61136eafa5ab0ad3b40b47e870846015a +size 54898 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..f1fb8735 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a67064363e3dfbdf2e6da482be84ae6db312044ef9b40ed734eff87a79a359e1 +size 46929 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..e9f3a461 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:646766d6b7c8929f79bc022959979324af375b1526ad5d361ae3cc77597598d2 +size 46862 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..1c27117a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93934e9955225c61a316bb8feed91dd2491b968ed75ca2ecc6de3f1541197c11 +size 50083 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..54ab96de --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a687dc7091288d2fdfe3d960736707de24f403e620c47ebe5d062e5887a79c03 +size 47032 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..cfe32149 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08af7e51359be707d1e406682a359063ce0bf0ed6d86e06d2b57bec3653ba534 +size 69106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..e1746658 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1b069d08819333431996515e47179bffd3d98eba862c2f928f0590b4111eb6d +size 69960 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..fa74f44a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f5df82b87a8c54f98f7258376e2a8806b717718c3406c68bcbf8113c685431f +size 78409 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..2aff2b5a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0fcc712b96b1de4d86316c39d57e228d5f367eeb2a5686cda27cc2f69da9d12 +size 69628 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..4ad8bf51 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d55df9a578cb52927de0dbea3064000ada05d6eb7f0288df03d72c19c8b426d +size 42841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..d2b94b0d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b94e105198bc07cf8d7780ecb79e90cdd1591d982d0e762900327ea41eb4ce5 +size 43217 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..8a35556e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b578757bac72f284a95a1a1cb05eb6ca885f91b4d28701f59af4e651335da5f +size 46398 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..791ca992 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f723799e69ff81b9059dc1d1d0c6041546ece591d2a1fdb58777f010a1e564f3 +size 42908 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..904189c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9381c07356c4ab9c3e029db81474fdc72828b51dbbeadb4982a127138a6efc9c +size 63549 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..35fa411e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0094a72a1cecf1bcd01c0374fde52a0f33beaa2a66ea4d39b3124c7bd080ea68 +size 65052 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..5467ae68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db7fce7d36f66bece6cfa24f8d089089d760d8256cb444d36e4d6b29c8df2e7e +size 72862 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..03c9ab7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockNewSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ade4c89096b2a4bd5c590da894d2b2577edf3ca356047c7f84d309c8a0f081 +size 64281 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=).png new file mode 100644 index 00000000..2e09bc34 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21cc4b392a403cb2079e16952b302f300225ccfab951db32e64eb9e5793c5d1f +size 16247 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123).png new file mode 100644 index 00000000..dfed91e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a240204c90f55d942ce1b076385ac601dd0c2d1923e0fdf3956686110176afcb +size 16431 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..745194c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00452c1803a5532b4b132b3e139757d2cf1bd9fe4607bfab84c7cd9981a82eb4 +size 16579 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..b0467708 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6745633180dac02c05c974253152a6c124ee59e985906dedebe7c121911ed73 +size 16736 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..06ca1610 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b14f331ab2a8a917e462259c45dfe31b2fe159d8cb44802377fb58f4fcf2192b +size 16635 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png new file mode 100644 index 00000000..ff99375b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:887f383af30fd071b34604b8eb74dc20ea278703651ab700a08332156ddd0796 +size 18423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png new file mode 100644 index 00000000..ce057633 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fccabc5a0502d3c791a55aed4f44acc9cd34531630a83cccbf077b7c6565dad1 +size 18611 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..8d86621b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6d3efde8d4f4b0e2f9c3c55d46ede6cf817be7b07424f6df25fdfc620f0a6c1 +size 18766 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..38d270cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fc579e21a2f09fcdb599cd537dc83ad3d0cddd6f16a0b17115036056037cef2 +size 18908 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..59e33536 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fe15048070ffc99d3deefa906905b1c8ddd4e23af9c40aa84dfb62dd503aca9 +size 18815 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png new file mode 100644 index 00000000..729a262f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0542dd818b223ddc04cf1cc61e391756848e81a99b48bde43ceb9630f58b2f12 +size 31316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png new file mode 100644 index 00000000..10c658f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc6d881d17458662719765f5e2ad63bfd7b97aa901e782a30de491fc44757cce +size 31428 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..80b4c5e5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5054b87210af94a57439831dfcd9a439a538ba55d070c48bbd7a04df56f4b535 +size 31699 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..b14a96b1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e1a0b56f949d5c9840b3bb1f3877131b7f620c1eb4a351d931b5ffa6c39e9bf +size 31980 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..3578220a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1ac50c07702f3a61e223dc148d8e3c9f3ef690de2cc6e5f018912b6a0d4c671 +size 31788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=).png new file mode 100644 index 00000000..3957da39 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e506cc15adeb27232ae7dbb082aa00d051dfd92d6eac81b65fcee44da9c7fc4e +size 14931 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123).png new file mode 100644 index 00000000..d7ab2647 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:150164f9bebbdfc26e4ba1b6454dfd9b94bcdbdca0846245d592876a89683f61 +size 15046 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..376bb42c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0747a9a48be8452d1d4f7ac7519c517e5e473cf17405054868f6056634a6bb +size 15172 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..8c2231ff --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0eb971bda9accfc67f27bca679ae8d2c71e8fe7ea212b5b03f90c06f0ddf4479 +size 15285 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..224ed585 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:486f1825c5ddc510934b72883c6115816dfed5eb8dac74b2b438351197718266 +size 15212 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png new file mode 100644 index 00000000..fd88fd6b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5426c7f2fc045520a751b81736a8be903304c1889bcdf321738d34fb9293d11 +size 16979 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png new file mode 100644 index 00000000..909c1a9d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5945694b943e0f18ef178add4427eb2169f08e80dc3cb45e11f3170ee4bb8418 +size 17095 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..0b5d6d19 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c710a334dc7c15c035cc4771cddadfb8519f0e6f5cdc53922bec348ab04208d0 +size 17225 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..dfe0ae51 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62092ad9fd785583b899e956fb3dca6ab552e78e19b4ec8fef3e0fb0330a67d7 +size 17336 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..f2a24aa0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c54abcd6ae685f40cc945fee51e83025ad824f5100348356c1d5db1ca19045d4 +size 17263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png new file mode 100644 index 00000000..9c51092a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3056038c8178d330487b681fb650510ef804023b758a01d0cbfa38966bcf75f1 +size 28512 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png new file mode 100644 index 00000000..7dacfabe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ade204c1a801ce3ce5f2caafdd2f81b8afac10ffbdb2311a1efa93252c31b45e +size 28634 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..1cc46d98 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8ba144e40418ebcaf75e7f0aa1d11071210d6a3b0f71e1f4f4f7872917b72be +size 28791 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..1211b338 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac4e59cc4d3501c2a26b7598a854b5b0e99c3d6f97b1c9369d2396fcf20c2a54 +size 28957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..e6b4f685 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed95fc39bb81e46b083d3f50809c681c9e5212a1c4584a704f2032bb1d0fdb80 +size 28848 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png new file mode 100644 index 00000000..d5349d07 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39692011a314138f3b212874dbb5b638c006920e1442b1a138bc1b745c405808 +size 21287 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png new file mode 100644 index 00000000..74dde513 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29f399f19e95e2bab96de7b3525ed0715616e5db2a289cef7baaacdcf9a828ea +size 21456 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..ccba952f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdcacd1339194dcf43820c64114f6d78260d1b8f05788a28c44e2b96d18197d5 +size 21610 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..5d1a3153 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc761ca22458ed85f30fb2df7e63f4d67b55f4c27da283c1bc4876a12cafb4ce +size 21747 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..fd9b153a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eac2cd4407e68329df97b56d384e6f97d6fd5e3fc790321bdf08e2b92d5fb12 +size 21645 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png new file mode 100644 index 00000000..2b6febb0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c469fc580f8dd3837c75b89527811340e2154a9789a2bf29ed25647a9eb13a22 +size 36411 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png new file mode 100644 index 00000000..f6f29f97 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54533283f56b1c1479cc353c1162e127487e16949d3f66cebcdcc9053bd1bf5b +size 36801 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..b96fa04c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba36e1637f2d710eabbf80c74f212204a5486bc3f6403f27b0f43ba2da629db9 +size 37089 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..fda1157e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7475688c34247a1c8d63e332069afb3337e3c49137ab5c873954db85650afa0a +size 37373 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..d1ce03dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9568a7c2ef8cf00c4b3f15e038f64693bfb87a8971d37eabb60e6d0dbcd578d2 +size 37165 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png new file mode 100644 index 00000000..5f3d8329 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:734481a16c27117f9e7652d0948fd683e330a6bb99118f1b626828439f1cb03e +size 19465 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png new file mode 100644 index 00000000..1a3541ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74ece2729a73bc3194c6fc015371db53ee14b0ddfc47625ae606265a3180c8b +size 19631 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..ba63871e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78b0bc9037c1dee23b7e0105fabd39280e9b7ee7c103654ce3e54b826a6c0230 +size 19786 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..6d46ec85 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a09cb5401cbeeb30625a4fe4893f0ba2e4d7ffa0f57803fcb4934d20f37fcdb +size 19926 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..0b37c52f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca247fa89dcaaebb12329936a60cda5b01fab7aca38e210bd6fd01b2b7eaa600 +size 19831 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png new file mode 100644 index 00000000..25d50f46 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae6a54063a1a3eda9a75451c2ad665f9a6868a71bad259c3e25ef635d1b43eb2 +size 32780 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png new file mode 100644 index 00000000..213beb4f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7371944dc7996b6220e50925cc560b006e8ce0f4fe8c2974e4aa9b9259007bd +size 32842 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png new file mode 100644 index 00000000..5b463a12 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9738a9de320d1cbec21427e14e088e100ec50b6f3200511c278d3c1a97bb3f59 +size 33103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png new file mode 100644 index 00000000..63d60463 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123456789).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f851b399c2f34a593d262369d1783c4e9087e7d4b3fcaf2163697d57439fc2 +size 33363 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png new file mode 100644 index 00000000..99b897f2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockOldSecretScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_pin(pin=123_123).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89814582dbafbc8c923d254c8bbcff39db64509ee084ec2b5255f6dd43d01e08 +size 33195 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_.png new file mode 100644 index 00000000..f26bca78 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fe801bf54c709a7e33790e8a3d8122ba71540724d4ce3fc68f907e9ced5f423 +size 13946 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123.png new file mode 100644 index 00000000..ca5b42ab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9697eeccaa9e761c3ae506647c0b2331e2bb0e647e08531e3d42d2c7daa8fdc9 +size 14471 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123.png new file mode 100644 index 00000000..9b3a5911 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0c129a0cee1c8b3108c73b094e610b8cce7655cd8804f33c5eb8df5fe16505f +size 14974 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123123.png new file mode 100644 index 00000000..8de8c634 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:783c396187dcd87ce0f23e12c53e2cf4d8313a04e7aa7391662c25cb73eeeb40 +size 15406 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123_123.png new file mode 100644 index 00000000..857bf83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8beddc2d3bded540d9e8f2bad6e1692ec5be4d72a889424b9aed3b2d1f8a2708 +size 14978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_.png new file mode 100644 index 00000000..05619e1a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de7fa6f4b398770a727f492b6c930488be53406a9282cd9fbaeee42ce0a4dcb2 +size 14832 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123.png new file mode 100644 index 00000000..f96f37a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd5b8b4eac0f664ad3fa92301e523463bbd4c8f8dc6dc4ae713d2cdb4ceaa7f5 +size 15367 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123.png new file mode 100644 index 00000000..1e13e863 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adc1aa62e205a533cfc1bb80dc185341d477ccdd74ce5ea584bd2a95d6158fd1 +size 15861 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123123.png new file mode 100644 index 00000000..d3bb2c2b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0aa5f093cce9bf937b3f9dfa2fe5d798e836f6a03ebbbafc08446da550858fa +size 16290 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123_123.png new file mode 100644 index 00000000..cda86f7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec467b75a728162de43a991f1e28d87a325199f7a0130bbb642b3d6363360456 +size 15866 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png new file mode 100644 index 00000000..e66af75e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:608927bf882c296c321f417e9a9bcc6dc889e39e325cf9e219e7ec420e91e907 +size 24029 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png new file mode 100644 index 00000000..cc2e7c78 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fc844c1a3680daad3a40da597d6f796323c4b3e70fb8f6dd0b58394c3c99bf3 +size 24726 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png new file mode 100644 index 00000000..81a2d231 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f74439c1b23ed4e442a67d060803248e702ac416071b0976d7c62bbc2956b54 +size 25457 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png new file mode 100644 index 00000000..01e144c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32755d622b1d25c840681918eabc325e905a447d5dcee2bb9ed9b313e044923d +size 26144 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png new file mode 100644 index 00000000..7e95586a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0596cd435bc447f828b5dec49da48aaef4720b1ffbf549fbf7d481ad19a40dba +size 25512 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_.png new file mode 100644 index 00000000..6b15da83 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ab8ed6094c204eb11ae503ec06d773ca90dcf8aeed5fd041f6920d56269c2fa +size 12625 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123.png new file mode 100644 index 00000000..00460128 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af95fa67ea597295366a896a504b19da2bd4a1875f6a082afb34a50dfdcfc9d3 +size 13073 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123.png new file mode 100644 index 00000000..8169fac1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:623bf030d6145366f8288308b3e360656c79271504a1990889073e148c716b07 +size 13465 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123123.png new file mode 100644 index 00000000..f114bb88 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d875582734a06f49e76f951d6c5e39ea9c3855edc1e8c9372681e71280fb72e +size 13839 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123_123.png new file mode 100644 index 00000000..8bc43dfb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32579ab1ab7a7182b1d067d5d8779d63cfaf3f44c498c9651b653428c765dd46 +size 13494 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_.png new file mode 100644 index 00000000..85e8dbd7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:751b6127d98c9facbb394b66cd691a76d81a248b063b26156afa6b4feed59868 +size 13440 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123.png new file mode 100644 index 00000000..7fb6a942 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac288fe68ea2ed9c85f202b9837999bb3df4835dbae8bbc83ddd47c15745616b +size 13879 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123.png new file mode 100644 index 00000000..f6383409 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71326a6bab626a615a6f5c4050bec75ac4cd76493857d768e42e440900cbd369 +size 14271 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123123.png new file mode 100644 index 00000000..d5502ddd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01d541cf3836338635c56d66fe37bbd8d441a93be0530a562ddd8101f75d03dd +size 14639 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123_123.png new file mode 100644 index 00000000..c911a3db --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1edd7f40cce3bfa9b15798591655d7d85add9609afe76f9f9831f93b087a7c94 +size 14297 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png new file mode 100644 index 00000000..1a9599f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c27156d1ecf534fa80e1e3e5aae04b867360649bcdb1bd5ae446cf7d6ab7597 +size 22248 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png new file mode 100644 index 00000000..4fdf984f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00bcc50adff29741b391494af121b0edf571216dfe77f2fa969c79ca8338dcd +size 22828 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png new file mode 100644 index 00000000..71d2926e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e1813137ccb2b72f6227cb0baf02f163827eb8c9ab5d7781ed61860194370dc +size 23427 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png new file mode 100644 index 00000000..ada3971c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be40c12ba0a152c33a1caddefdbc7e0807ba9e53c81d9bde53311f97d1265eb8 +size 23970 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png new file mode 100644 index 00000000..b335d0b1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e18bc9b70b9e055cd87d130bd61402ca0f32da197f9ec055f9d74b4a497610bf +size 23487 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_.png new file mode 100644 index 00000000..b7d7df27 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaf2e7764b2eddbd46f3bd062f6f168b4feb3790389af1b1f76a37070ffce1fe +size 16933 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123.png new file mode 100644 index 00000000..a8cc4f99 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:889cc8e58a0af6f098f45a5d41aaa0efa1fce157732bb14e1e98aa56f926546d +size 17496 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123.png new file mode 100644 index 00000000..00ff05ef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d428d0a686becbfff6766364edb56745e76e3ec0b25364a64dbbd13105a12b +size 18080 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123123.png new file mode 100644 index 00000000..f3b04afd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2c0c7b6e84a1339a3ac5ee7cf3b4e3b738439c78edb2a2fd5a03272a7afeaac +size 18581 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123_123.png new file mode 100644 index 00000000..6a81f14b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ca769377786b23bdbb6c3d69877e94b9267a1efe7abd67bed37d4044ce690c4 +size 18103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png new file mode 100644 index 00000000..0a5f5d5c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c062deceae4c32712a31095bdd9d4692e6d8a87a79de0931a777fa846dc8a3f4 +size 27452 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png new file mode 100644 index 00000000..4161fdf7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3359eecad35fab0f2a0bd8ec6f310cc9b9ccca5c871abfae1eeee520f54a57c +size 28269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png new file mode 100644 index 00000000..457d71f3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c34c87545ac79bca97a17eddf50a1a806624bd5165743c24e42de0abe9f8d10 +size 29020 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png new file mode 100644 index 00000000..88050732 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48d092af5b0dadedb52869d176095df83f27455b6175317dafa4e62815f443ac +size 29685 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png new file mode 100644 index 00000000..ebf1945d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79e3588f0dc60abe241ba0444a484a4802a570f93de755d9bc6408577267b8a5 +size 29028 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_.png new file mode 100644 index 00000000..df7cb5fd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f0a7eeacb196023e92b75a2e6afc09f14480058764552a58211a9e3955b24d5 +size 15274 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123.png new file mode 100644 index 00000000..4194ffe0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c20e7d04bbeb5d48c7e6943e746258562ceb82350d5f65113c6dc73d149b46e +size 15755 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123.png new file mode 100644 index 00000000..e9924715 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f786ffb72e7adb3fc2ed0d349df2364e1411c67eeab798d2c6939d3d36536606 +size 16234 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123123.png new file mode 100644 index 00000000..ffe50ddf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c7a1782c6de891ed22f37cf9e7076d5090ffc2d879b3c489a01669a18b22669 +size 16639 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123_123.png new file mode 100644 index 00000000..10494d40 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e07efdc9a967109c5229df6ec7db90cb568c932a4ddfee123d35b797aff8883 +size 16252 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png new file mode 100644 index 00000000..b3f7cf09 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2e1e2354d03955385ae0665d370624e3c0e20924e8dec2e8d467cc816e9bbda +size 25151 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png new file mode 100644 index 00000000..98b1ab2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07d9e2b5281fb7251b5455ce691053177d1e0f60cd5a7d1f09f6d08ee8195a42 +size 26029 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png new file mode 100644 index 00000000..60c3b031 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecaf94a3b4445ea1b25bd8cabb0d11f04af3b786f61b3cd33e7e5df111a09dfa +size 26795 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png new file mode 100644 index 00000000..f42904e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8295f75ff78bcff0d328ebe3d395a04661aeab52e1332059aebebb30e527f7b5 +size 27443 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png new file mode 100644 index 00000000..22d049cf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardunlock.ui_CardUnlockPukScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f949f158f31fa2014f9cfb45acd020c42e585e6c17fd825f197ad115445e3aa4 +size 26818 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_.png new file mode 100644 index 00000000..20947cfc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5805b2b8c61eac68b532e299678f19aeef8e48bdcfb02b32aacb45cf415bfc57 +size 142908 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123.png new file mode 100644 index 00000000..8861f6a7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1d417a03364bb2365c1fe2d7739d7aaa4ca86803044c16e8089812a09b0a86d +size 143282 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123.png new file mode 100644 index 00000000..868ebffb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d67e0d4a4503c19fa0653fd8802d485ed7609caa45e13a4d86344de19f0ebb9d +size 143729 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123123.png new file mode 100644 index 00000000..81eb52cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca4b6b541898eb546963d4a82dda73020f01b94022bf2aa222f6d071eb771496 +size 144148 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123_123.png new file mode 100644 index 00000000..4787a033 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94bec1e32ef1b45cca5f0a1b28b700f12720583adf030f183869b04afabad895 +size 143742 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_.png new file mode 100644 index 00000000..0ac5197d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0481caa173cb88ae3078c7051f0f1e8521ba77d47ff9cb4c96dfbbb2808c466 +size 144449 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123.png new file mode 100644 index 00000000..485cd2fd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9a2e8c1d9770a56c52b5f57a6659c7177df90d5a853d94a4b5b6d84989fdd56 +size 144561 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123.png new file mode 100644 index 00000000..955964e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f338fe37cb216977c258d9084324e25cd61144c624d784f64a20d96deda4406b +size 144995 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123123.png new file mode 100644 index 00000000..df0c2b08 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbbbe83faa8230f3bf0ee5af255aeeb851223952f08d207d2d94cdd8ab2bb8fb +size 145406 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123_123.png new file mode 100644 index 00000000..529386a2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0548dc4c8aa1c9d2c55c92eebf900e64692ffa38414014928b152a159d6bf922 +size 145028 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png new file mode 100644 index 00000000..424f1e56 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6ef1c5141e18304eba2724f431742043f79cbb2bcb15d33a95d292dccaa27b9 +size 167276 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png new file mode 100644 index 00000000..2538de70 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fe9fe738ef94ad7b237f39bdebb0724da6e3a8fe2b24b898cb514b1c29a15fb +size 167826 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png new file mode 100644 index 00000000..f937c9de --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec2c450bc510b6c23826e740f9debd65e25d028ba57de9340ffca2e86d6fd234 +size 168551 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png new file mode 100644 index 00000000..461688e5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c12a6c98f89ae9e666fa8dbee6f79048ad9867eaf2859a6de336e0dcf1f2bc25 +size 169240 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png new file mode 100644 index 00000000..c27b2660 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b76349c2c065a2fe69bb9bc955bb11122126cd05342905d13a51160f631e691 +size 168614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_.png new file mode 100644 index 00000000..82e18326 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768d94c0ef6225b05aec51a4e5b941e316379965978b8590e2c467261b849c8c +size 140820 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123.png new file mode 100644 index 00000000..7b9a692c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3450672eb2cf90876d8315c11fec5333ab7faba5801f4dc26a08cee5c68111f0 +size 141187 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123.png new file mode 100644 index 00000000..a2bd17ab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63f0ba2ee9d33e8fac90086f0c72babdf9f473b33e6770968d2972e0015663ad +size 141562 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123123.png new file mode 100644 index 00000000..b7d1aff6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd85d551e96d372eed1af934f3102fecc75f5c9316ab8d29ffc7b63fa3e4a065 +size 141894 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123_123.png new file mode 100644 index 00000000..31ba0611 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf2fc5f748d63cba69360a9881ca1d24ec3c1c42a65474a62f68aef1685c8f19 +size 141582 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_.png new file mode 100644 index 00000000..f80ea6d7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b818eafb881b63702a0f8f444f0df8d52e3e5ccef42c7e857dc603f5d79b6df4 +size 142065 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123.png new file mode 100644 index 00000000..aa0bdbc7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e027715be2bdf426abde7c4e26020317a8b5a503288181252591dd93e6ec1a8d +size 142251 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123.png new file mode 100644 index 00000000..e2333b3c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:404021f00197f6c7a8231381a54315eb2b08421eccc46c02d2623b97545d9d46 +size 142621 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123123.png new file mode 100644 index 00000000..65292018 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:584710e87ff54ca011539d4524c26eaf13545937258e6d55a42c1528a1b4e2d6 +size 142964 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123_123.png new file mode 100644 index 00000000..99765a28 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83eb35cfa435587ee03b111570f1f8782a24710d378ef0536f358751ac896bc5 +size 142649 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png new file mode 100644 index 00000000..a784f502 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:949330ac48f66334ecc68985a8618953b7c80b9dc5ed2c0a5b50ec56fa6e95c3 +size 163373 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png new file mode 100644 index 00000000..6c38d200 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2994cba8812f371650c59e651d48837033d4fdc3b787301904e9bc62fc0d2f1 +size 163934 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png new file mode 100644 index 00000000..7748c2e9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:377c681fea1f5a99f4f0b1fbb92fcc3703369a73cbc27669d38b09a7cc041b9e +size 164564 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png new file mode 100644 index 00000000..e00c4b84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4127e11ec317a324caf22a3752a0f891c858273eca6f5578b06d2355c6ee851b +size 165136 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png new file mode 100644 index 00000000..b7fbb2bd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8bc627c07e6a3b36ee888d089c243cdc7cb1f4feccdb9eabf526f8432379b77 +size 164622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_.png new file mode 100644 index 00000000..f6f38743 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:929903c30712cf36401f702c519e7623ffbc96d583d8d4762a0200c535a604ad +size 151922 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123.png new file mode 100644 index 00000000..ae0b30c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e0aee3266341f361dc7bbace4fbe001bdee2c78358b28e9af15fe272d22cc54 +size 152058 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123.png new file mode 100644 index 00000000..945ace67 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d23c591f43021ac5834887f8be1036afcabad06ccd15b43cadeb88155ee238e +size 152475 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123123.png new file mode 100644 index 00000000..45032c07 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f988d2522d566d311910b307b7194622234102bedf0f83bce5d2f654b03bd36d +size 152820 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123_123.png new file mode 100644 index 00000000..4730ccec --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35afb56429e6f8927f3df52ea5fb975204b85f76ca75c5c91983a0d62fde526b +size 152498 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png new file mode 100644 index 00000000..f3a9b044 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fa0806ee933c1d3d90b52150d9cf2ccca87cbdb76cdc6ca14a11e1e1bd0dbb1 +size 176786 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png new file mode 100644 index 00000000..1f07e69e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7a42f8550b701f1dac85b3ed9a6aab860f9ffc4d84d8b424c83c371e2dad05a +size 179225 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png new file mode 100644 index 00000000..0b5be2cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e285e6109450563fa21debefb20d417f7d57a12e48ed67b98ea3f4e75f2c494 +size 180164 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png new file mode 100644 index 00000000..e0dc80ff --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a235653017b78b859f66db84df54d019cab1db9e690daa45295554f5a6e94c97 +size 180997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png new file mode 100644 index 00000000..237f7615 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ad23986344665edd27269571ce14052efc4388cb80ec9de49326c3cace84ce4 +size 180190 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_.png new file mode 100644 index 00000000..1b1204b5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e0721b97e7d5f6bd788778b491fd3f4db22295ae0e8ec46a3c0277a4c336718 +size 149525 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123.png new file mode 100644 index 00000000..0d88bb81 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc142ef608dcb31408addf5b82162454c486f09a8b52e6785681a47b8c6db0b1 +size 149705 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123.png new file mode 100644 index 00000000..eb7c91ed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3f298c9f92f610f5f0b1af4e6fc688441622140c9df53ab7f3038f1c227ccfc +size 150169 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123123.png new file mode 100644 index 00000000..ce5c992e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaa4e3cfaca794967016d4df4b4410fedd60c3282c53806bc82d72de005a2c14 +size 150562 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123_123.png new file mode 100644 index 00000000..a51a76dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337d33db41b25a23babf94d2eec6a334d3bc406f17eddf9b94d466046e428292 +size 150156 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png new file mode 100644 index 00000000..e663b5d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7806f6bf50f8cdbed563c9e95ed26d556a0d8c1ffdc3e8bdafbf78e0f2ae2555 +size 172315 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png new file mode 100644 index 00000000..79d6b6b5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:548645d968b796362e50f23d06f578e2c2a11fa60856d279749797bcfd192ce2 +size 174549 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png new file mode 100644 index 00000000..5c10cc03 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4314521f0b06fd6a3dd248e980f92734b133a9877f53cc156dd1c4089e4b881b +size 175353 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png new file mode 100644 index 00000000..13443623 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123123123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60e85433b51ac784381edd9f464af8e4d9f0db7a00da0c1d4bb41ffa8c2775c3 +size 176078 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png new file mode 100644 index 00000000..8ca5557d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallCanScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_can_123_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79f868f65142d106219fab4929e8d128807ea480c16db42116aabc7914a4bbd3 +size 175364 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..305da5d6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b37c98d4394c24b55660ea75b4b6992a8f288f7739001f4511439f5bfa977fc5 +size 117824 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..488cba04 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:397b4f840e6a135e15bb24c4f858df810483ddf64b75022dbeb698f115ac67aa +size 122731 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..db075899 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec529b91a768cb2f165fa88b21e089dd5e31c4f9e7618aedf7e994629d2fe4fb +size 89407 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..f65a952b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00e68209eff66d5899d45cee59bc535c2a634c590575207a230e7d8583f4f366 +size 115223 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..37b3a93e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ade6f91d3b4a036a186adf1ec02597834ecd81ccdcc95ab03e9515b95c3fee29 +size 120190 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..88c85be5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd48b56ad3a4b39e5f19617d14e8eb0fdb45f6cb39d10a887b4368741675b76a +size 88387 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..7956bbfa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28452c73f8f73afb664c203ea731d4ba4a795743806c3f29ae4cf68a0b690833 +size 134293 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c5ce8d71 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51e70c069350d5715a43a34c15b0ba7f06af50bdb228b20138a918ca945c37b1 +size 91042 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..26f0b236 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4490b3f10f05a5b996cffc119bff809bf520771d90557f930217c5d9f372994d +size 131222 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..9fea0fb1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidHelpScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7470dd033ddbbf7dcbeb1025606b122749517284019e26acd691ab1b7311534f +size 89838 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png new file mode 100644 index 00000000..bb8a1906 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8dd197429b67905646c9405ce4f413a3252affe03b730081f1b77ffa77c3710 +size 22423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png new file mode 100644 index 00000000..a095e57b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d0b93bbdf7e7e46cd6e8a6a6a1a4742150e3b9b1b7b34114d1f14430890f5af +size 22316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png new file mode 100644 index 00000000..57789a7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:141b6951a4f9225f5f0e23f9b6aa11561c63ddde7f7fc06b7f9b40324295a77b +size 29761 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png new file mode 100644 index 00000000..15d500b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48332eb3fb92859260830ef230e21a1e7e29d101312e07850db3d40135a245f4 +size 38454 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..698660cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b8d23c9d5dd7ef0e9c76bca0118e9244ebb24e3005ae5a14ba7dcf3b4c2bc36 +size 24038 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..f0232cec --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43d5a547516d51607a1e7eb0c289b09dd451ddcc1addac503ab47571c71dabdc +size 23927 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..6e58a2dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edbcf2f5a056c2a75b66a55d41d07bf739b108353fa06c730880688234d1b649 +size 35716 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..52b13743 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5cbcf9482223604d10869ba055e50e7f960a2ab8753f38c3567223502b2ce56 +size 40226 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..857f5ce5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15444f8ace5ea8e914ce6ec3f3133394737a4e359df6a46f6f04eb1277a1e5a6 +size 41678 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..1a08ab73 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c920e2fd54a854a79338c162587ddb2938b347121f1a9730bcbe8564a54e74ca +size 41543 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..eedea9f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5212c99f6db1c4e2e104c9fac32f4d6cc1b59395012581890e8ed09bafb0aa42 +size 62378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..2efbcbb2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:779d4fba66121eb6d00702a7dd0606ad6bc47121b0dd54a168d7fe6a9edca832 +size 55289 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png new file mode 100644 index 00000000..137e752a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b95c8b2432aca450e1f2e56348babab7ab7726a19c792aad9d64d8e16ef3488 +size 20980 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png new file mode 100644 index 00000000..97ba383b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b58e3cb35e7d1ac456bbb315b4eee8b31cb1f52ec5b1ea676286bf30baae9650 +size 20855 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png new file mode 100644 index 00000000..ed086c22 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f04b71dcd385b54f5403228a328a72f17e9ced99a74c7c2ff559765fefb32be +size 27189 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png new file mode 100644 index 00000000..ab7d8b73 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1310696b0e762c942764f47458f831802902e64456cbb490e29e5c5f3a3ba12 +size 35712 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..773790a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7529431b7f58cdf502867dc64f9656b53aa80355be26c4354cf9470861f272 +size 22594 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..2f247136 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0428af647d2e56de01cf00a4459e9156510e3e3af0c1ab507a8392f03ebae21a +size 22483 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..46b8721e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30eeb3f7db83cdf1204052139be658480f623dc4c19b7430ce77967490e3060d +size 32546 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..f2c73d01 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e91e2c0250bbd0d3c9b94854e0a4a4a1126643dfe8527abce66c69fb8a7c9d02 +size 37382 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..f0305454 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c9f7234f4235fa4495705581f6a7358def42f1e9879c14fd71de0774612390b +size 38178 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..51177490 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca097d9ebdd16f858488e52aabbeee42d7ab5f99e66cbcba475e66c346d98fbd +size 38029 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..568f2929 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:449d6574ee8f852fd278439e3de7089cf72cbfe2485ad7dc2c82dc14cdf0408e +size 56650 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..38148eb2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0121914a691fda10a374ef1f4d34cbc472a4d2cf84ca50f8e2964a296e30db55 +size 50829 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..117e7be5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64b44a21f9df8303e9b43420904f953c22b76c2c26b94e3d3c22a35172495b47 +size 27345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..8ddbfccc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e65a4f2a76ea5bb725e010259cd3c596a36f9323c212893d3c487b29ea607b18 +size 27082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..1720e283 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec8a0b7dcb8b98d2b0ce47d5fc445778003019c3d79a2f3fff30ceadafe8c6a4 +size 40927 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..14b434eb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ae46b43a435ab800c02aa852644666fb6a97e105ae80b2e74ce72d5b051793 +size 46102 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..d7d470fc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa50b6301ed71dd0ae1fecdb4f1aff50f03bb825d08bc5f328eb4f7fd96dccd +size 47674 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..413c39ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80313bfdf4e9cb603e7bf19b087f33433f3fb77020df7368cb37f7a17c844589 +size 47532 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..a8f16e98 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7fa8922f116d91f38d48840bbef956528024695ecde0b8b88f6740fd306eaad +size 72072 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..7861f616 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dc2d2b32e93ef570e7c1e1cd1c76fac385204b72eb0462edc97508e600058be +size 61471 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..a904b988 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5052d798144c51451fee32a32a3059e088c6ad2e675e38c4c881e57ac3b131ab +size 25432 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..23fa9ea2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04c8d5a9e9239bea8653506071d72c675675f57e97590359876c6f89fed1f0ac +size 25318 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..39dc1630 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03e47c29a16edf1d1ca43993147c6fb8d974cd7bd7422df8b854731f8e8e3409 +size 37289 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..b8d36a12 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32357e8369ac4ffb422671f1708b80860176c6c2730869734de5aa55756323af +size 42555 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..9fa8016c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20606e52fc0df50fd3150228fb4f5b329aff52a2b7b4652b152afd46d8fd4358 +size 43209 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..0e8cb32f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e93677987abafdc5a8110109ac5945624f1b3f94cd85433cec716ef2542a0dbd +size 43056 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..eeafddbb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c4301378f880a9e78a1d5242f48e6b126bd8cf3eb12e847c2018651500cdd7a +size 65291 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..96895ab9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallGidListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee265902e2477158f00a962d73cb9434daf203ee037277f6d5344ba2bf028a6a +size 56200 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcenabled.png new file mode 100644 index 00000000..6400d68a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d418bc9d9bdc00d09943a27b6d204cb0a1878ee4919f4b7873dd3f3c50b8d84 +size 236118 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotavailable.png new file mode 100644 index 00000000..98cc7df4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:833f129232cbca1948da9b76629b5496b4e45f6a3bc4f0b46398b7035ea29920 +size 231573 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotenabled.png new file mode 100644 index 00000000..6400d68a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d418bc9d9bdc00d09943a27b6d204cb0a1878ee4919f4b7873dd3f3c50b8d84 +size 236118 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcenabled.png new file mode 100644 index 00000000..214c606c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d14bc6e8623bc5169a3036312bac118ed93493b64bea43ee85d4a98c59301b24 +size 248475 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotavailable.png new file mode 100644 index 00000000..f314e11b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a40fe5f34b83d8ebe434dee4c22213a08eb2b339800826c19ca43500dd6b1e94 +size 244566 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotenabled.png new file mode 100644 index 00000000..214c606c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d14bc6e8623bc5169a3036312bac118ed93493b64bea43ee85d4a98c59301b24 +size 248475 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcenabled.png new file mode 100644 index 00000000..2adf587d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:745ffd16596bb48b19a55345412bb8fa5f62007136d42bbc2fa307f307c44de2 +size 222341 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotavailable.png new file mode 100644 index 00000000..50d13fa1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad9d2110ce75e472ef1440704026e328b98c12a7a5fadde7bbf80c2f5cb50034 +size 220154 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotenabled.png new file mode 100644 index 00000000..2adf587d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:745ffd16596bb48b19a55345412bb8fa5f62007136d42bbc2fa307f307c44de2 +size 222341 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcenabled.png new file mode 100644 index 00000000..7d32047d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64818554aab84079f0819e6bf233bbbe415bf6b2b64316e836ebd1382024ea7a +size 230416 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotavailable.png new file mode 100644 index 00000000..e07bdd2b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e75efaa6ca97648722be968eefafbe85e18445e411a710b9f2666827ef60e9e6 +size 228811 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotenabled.png new file mode 100644 index 00000000..7d32047d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64818554aab84079f0819e6bf233bbbe415bf6b2b64316e836ebd1382024ea7a +size 230416 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png new file mode 100644 index 00000000..588cdf81 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d721503527f6c80ad175fe2f6c9f2bda09ebefc9e92b5aa5facfb183cdd36546 +size 232309 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png new file mode 100644 index 00000000..395d3d93 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8826860a4d02d1d4b647c5a24cd9d158ee9791f642f98d7205bc5185f9f49d +size 230941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png new file mode 100644 index 00000000..588cdf81 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d721503527f6c80ad175fe2f6c9f2bda09ebefc9e92b5aa5facfb183cdd36546 +size 232309 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcenabled.png new file mode 100644 index 00000000..a4accc95 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:856ca5a0a0409d35d9fd7d60dc1916d9f3b1008be31772c422e8e19e55265504 +size 211263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotavailable.png new file mode 100644 index 00000000..fda72ab9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dae66bf69f49f69e30f2ab9383da1922dbd5b3dfacd2bc99dbaee74d58f5cd4 +size 207881 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotenabled.png new file mode 100644 index 00000000..a4accc95 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:856ca5a0a0409d35d9fd7d60dc1916d9f3b1008be31772c422e8e19e55265504 +size 211263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcenabled.png new file mode 100644 index 00000000..0356a553 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e225e1407741bb43885584854c890e18bd379283533d29efcd0f8807cd98f9d +size 218386 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png new file mode 100644 index 00000000..46da8f8c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:359f5314b469c4f6b14888fc7bc259db3077c85edd659489eedaf91c48cb11e1 +size 215706 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png new file mode 100644 index 00000000..0356a553 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e225e1407741bb43885584854c890e18bd379283533d29efcd0f8807cd98f9d +size 218386 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png new file mode 100644 index 00000000..45df0578 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ed81c4b41fd6b58e025fa07e2caba1454a03478c754423b6221e7d36def9d98 +size 220802 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png new file mode 100644 index 00000000..50cb08a2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c60b524773564f90da862946dbf699a9f6dc8220e20c36d2805a8b47799cdc54 +size 217732 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png new file mode 100644 index 00000000..45df0578 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ed81c4b41fd6b58e025fa07e2caba1454a03478c754423b6221e7d36def9d98 +size 220802 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcenabled.png new file mode 100644 index 00000000..5c0c996e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6959769addc421642ec40604a7d5be7576fa1ff6b4a87f6c352621152b33acdb +size 244696 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotavailable.png new file mode 100644 index 00000000..4b561b7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97bab1d1e76a8687e95e20880e23246340ebc6b2ede84560071086d3553465c4 +size 242918 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotenabled.png new file mode 100644 index 00000000..5c0c996e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6959769addc421642ec40604a7d5be7576fa1ff6b4a87f6c352621152b33acdb +size 244696 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png new file mode 100644 index 00000000..a3b5b46e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b5762a610381a5d4c62221400a79162b90ca21a7c6edbf03986ff75e67bac3 +size 236790 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png new file mode 100644 index 00000000..4c2d5525 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01dc52efa907c7bd102c2144f4e7a6a1a6d1c16403ba6ae796d37434c082a7d3 +size 233800 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png new file mode 100644 index 00000000..a3b5b46e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b5762a610381a5d4c62221400a79162b90ca21a7c6edbf03986ff75e67bac3 +size 236790 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcenabled.png new file mode 100644 index 00000000..eac99877 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79761dfd7ccd919114bbf69be36ca6d250328f1181490871339a88a26b1c2e5e +size 230833 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png new file mode 100644 index 00000000..c030aff5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06978ab1fa4fc0211d5608c104b8b7768f79104a551f24553ffb6dabca6c59c1 +size 228207 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png new file mode 100644 index 00000000..eac99877 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79761dfd7ccd919114bbf69be36ca6d250328f1181490871339a88a26b1c2e5e +size 230833 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png new file mode 100644 index 00000000..92a32307 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d3a258f53d7e9732c156f1fdacf0ca2159d6f6b080f809818449175d511bb27 +size 224831 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png new file mode 100644 index 00000000..5b74d4e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotavailable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12a0d00d2af580c11fe52977030d219a3b430f97f9868f7c611c03daf3b39492 +size 220449 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png new file mode 100644 index 00000000..92a32307 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nfcnotenabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d3a258f53d7e9732c156f1fdacf0ca2159d6f6b080f809818449175d511bb27 +size 224831 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith3digits.png new file mode 100644 index 00000000..785f742d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46e1c945000c6c5bd249f02bd30d1c6b9e860e2620c526a922d70bb98661837e +size 31451 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith5digits.png new file mode 100644 index 00000000..42514cbb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8963d0317fec8a5f05a18ad2048e5203d57ec1eb59dce2b5526bcdf63071614b +size 32078 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith3digits.png new file mode 100644 index 00000000..4556c5f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e858b0b352d33592472009be5126ac6183f41f349aaf5e5628297c136378da6 +size 34080 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith5digits.png new file mode 100644 index 00000000..c33953b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce916f8db14380602b42a3d617c02287891c473ae7827dea1a37457fe7dcd664 +size 34749 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png new file mode 100644 index 00000000..bee543cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c24b241e09bf4c30cad51e37229bbbf5554e18e5b5d70848d1f1a3abbd9f2159 +size 58638 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png new file mode 100644 index 00000000..ba03b836 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29808166d9534bcde34029242d926652c95f46e64f0743e78416f153dd6ea7b8 +size 59668 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith3digits.png new file mode 100644 index 00000000..c1855f35 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:248dfc2d23cd802644cd91055803f80e7efe49a2ecb063621473ad2905b820c1 +size 30677 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith5digits.png new file mode 100644 index 00000000..cedf121f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:466ea50ea1c3857cbea1a48f8906f66cbb8560b71f0758dd1fda17708c22ad85 +size 31136 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith3digits.png new file mode 100644 index 00000000..9a912f04 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf57d60e5db887c92f9e969395f908054e7a572f1f91f1c0b06a6bdfeb4741b2 +size 33458 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith5digits.png new file mode 100644 index 00000000..cee6abef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd24e15b2e8f7520a39f411a4807f999e9423ba47b4b68ceecbb90826da70282 +size 33940 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png new file mode 100644 index 00000000..233b5adf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f22210e794968667915996004072ef08baa46aae28365c2ca1c0e0594c5c495 +size 57567 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png new file mode 100644 index 00000000..2c04d78d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dc7f745b4c1cb9f01bc4861f168027346b31ec0612236e53c3fed304c9a523e +size 58258 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith3digits.png new file mode 100644 index 00000000..64868a12 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7d55db986ab2229197ded625548c70ca12217bbc9407163d1e94d4b3b9102db +size 39569 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith5digits.png new file mode 100644 index 00000000..d0b92e5a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5ad825572f32ceb439dc55869e50f2149e727974b7f035a0d95f3745b59264e +size 40384 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png new file mode 100644 index 00000000..cc63f44c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ce415e775b2daa3192d2845ede244e2076ba19e8fb06ba8f0cb4bbe5423c226 +size 69224 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png new file mode 100644 index 00000000..befd93e5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ea0e8a742d3750dcd87805e2223a25e9ee1b91e7b94e91ae1b7f622e00b1788 +size 70364 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith3digits.png new file mode 100644 index 00000000..7d404e91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fae481f2e682a8d0cc85c04b442c0389635bdee1dd72db59d6625866e43c31c0 +size 38351 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith5digits.png new file mode 100644 index 00000000..b47a1c84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a02179dca9a5569cf6e870be52dcfc1dd2cf98935f82250a1f0aa33a4adf2c0 +size 39021 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png new file mode 100644 index 00000000..e1493106 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith3digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83a904935f1949727e713dbecca87d115543d96e9c01cc1bcf3d79b3272eeb84 +size 67594 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png new file mode 100644 index 00000000..67f26d41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallPinScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_pinwith5digits.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c08bed595a447ba11fa4c8cd9b4cb87a52ac773a14063c9e871ab32f20ca9c42 +size 68455 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png new file mode 100644 index 00000000..69444dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:121ed1feecf8ba09284663bec807944a0be4adfdd715a8086b748fb395557b69 +size 23844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png new file mode 100644 index 00000000..fc8c612a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f7e9a73764804174d7b46b441c65a641730c33de766aacfe4379498e979eb27 +size 24100 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png new file mode 100644 index 00000000..a1b9433e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634cfdf2409bc207c46a2d32d30bf71274a4083370915db5714d7dbe2cebde95 +size 23978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png new file mode 100644 index 00000000..dca91216 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51412f0f404b24f6827aceca7c0d0d0035eb954a9e9ab4c3915442e81d0cb868 +size 22788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png new file mode 100644 index 00000000..b04a37fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d03f7856a947194fde4febd99ae8ef0b4eae368c51559158a3b9166530846c +size 23957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png new file mode 100644 index 00000000..a5e8d671 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfecb3b7c54215e6a69da10119a51eff7e9f6652e6a565c8f921215671b8ca92 +size 27924 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png new file mode 100644 index 00000000..e1459497 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c32e73d52392e9adbfd6681cf36c9ec3f3668b136dabd658e2d5b1449d298319 +size 28003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png new file mode 100644 index 00000000..07aa86ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc1961bb8706ec632aec40813dae67d144f30c6e30ff72d8a8ba111d5f0afc1c +size 27996 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png new file mode 100644 index 00000000..24af1ef7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e46e8d7cd5d5282052c7b1e17e864ad131421953aa2b7d4695ed722be4d5875 +size 26378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png new file mode 100644 index 00000000..be4271aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b24360a08c1db1399593d3739dde2c94ca88e53f751c54353908168126911a9d +size 28010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png new file mode 100644 index 00000000..69444dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:121ed1feecf8ba09284663bec807944a0be4adfdd715a8086b748fb395557b69 +size 23844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png new file mode 100644 index 00000000..fc8c612a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f7e9a73764804174d7b46b441c65a641730c33de766aacfe4379498e979eb27 +size 24100 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png new file mode 100644 index 00000000..a1b9433e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634cfdf2409bc207c46a2d32d30bf71274a4083370915db5714d7dbe2cebde95 +size 23978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png new file mode 100644 index 00000000..dca91216 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51412f0f404b24f6827aceca7c0d0d0035eb954a9e9ab4c3915442e81d0cb868 +size 22788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png new file mode 100644 index 00000000..b04a37fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60d03f7856a947194fde4febd99ae8ef0b4eae368c51559158a3b9166530846c +size 23957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png new file mode 100644 index 00000000..a5e8d671 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfecb3b7c54215e6a69da10119a51eff7e9f6652e6a565c8f921215671b8ca92 +size 27924 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png new file mode 100644 index 00000000..e1459497 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c32e73d52392e9adbfd6681cf36c9ec3f3668b136dabd658e2d5b1449d298319 +size 28003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png new file mode 100644 index 00000000..07aa86ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc1961bb8706ec632aec40813dae67d144f30c6e30ff72d8a8ba111d5f0afc1c +size 27996 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png new file mode 100644 index 00000000..24af1ef7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e46e8d7cd5d5282052c7b1e17e864ad131421953aa2b7d4695ed722be4d5875 +size 26378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png new file mode 100644 index 00000000..be4271aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b24360a08c1db1399593d3739dde2c94ca88e53f751c54353908168126911a9d +size 28010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png new file mode 100644 index 00000000..120052d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:560a5ab6a035b70473ada37932c1e2680b82deb07182fb9b991c10cad6d30a4c +size 55413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png new file mode 100644 index 00000000..3114853f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96536552ae8a67e9d475c7e020b238750f60ebc8df59a1bb99847fd67747ece +size 55770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png new file mode 100644 index 00000000..b4a6e699 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3afda8cf9192f21614fc9da8083ef5cad2f82dd5799d6c97b6835ffbe132efd7 +size 55857 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png new file mode 100644 index 00000000..16608c4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68cc0f51573cbdd611b61e5a5caabef2f22c9b18ff12fdb5a32056bf2a52a3ed +size 52249 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png new file mode 100644 index 00000000..9341e299 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7abf232fbc1326a6fd07a477bde5615edbdec69e941ca4dab0ee470a4a80088c +size 55780 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png new file mode 100644 index 00000000..120052d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_top-left.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:560a5ab6a035b70473ada37932c1e2680b82deb07182fb9b991c10cad6d30a4c +size 55413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png new file mode 100644 index 00000000..3114853f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_top-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96536552ae8a67e9d475c7e020b238750f60ebc8df59a1bb99847fd67747ece +size 55770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png new file mode 100644 index 00000000..b4a6e699 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_top-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3afda8cf9192f21614fc9da8083ef5cad2f82dd5799d6c97b6835ffbe132efd7 +size 55857 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png new file mode 100644 index 00000000..16608c4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_center-mid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68cc0f51573cbdd611b61e5a5caabef2f22c9b18ff12fdb5a32056bf2a52a3ed +size 52249 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png new file mode 100644 index 00000000..9341e299 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallReadCardScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_bottom-right.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7abf232fbc1326a6fd07a477bde5615edbdec69e941ca4dab0ee470a4a80088c +size 55780 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..f32ad23d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89eaea46e090b6a3fbbf624deed279dec6a87772fdb2530091c8bb260e1fe2c8 +size 47882 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..eb1d6d31 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a0dc22f0182e6a15b5f5d949b2ba167ca3099f70de84e58c6e620f32299d12a +size 19053 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..01cbee13 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac281931d9eadb7a52aae57158ef013e4b1fb3b0849061b09d5a5cae35a0c9f8 +size 48614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..e66fa420 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67ebaaac70239cbe4c6c459bcb602a45f7e28137fb3582039405696e5fc9ea1d +size 53197 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..a1966987 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bca6ca5456c35f5324641a9967e456a4cb52f9938adccd9d774d1875a7878a96 +size 21641 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..3b4b210a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:597482a1ff402b3d41b881fe3eb38c810d141da8973fd4d58144406c3b31f662 +size 53987 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..ed5bb1ac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b175fe8cb0a8a15e926d830831fb01d6d95a9c4035fb51aba5aaff09a9dbadec +size 63006 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..eea7e5cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4712bb44426aac315bf731c637373ca873fd075d4f8f18b04fc1fd7869b44bd2 +size 29779 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..3d458943 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f28e2f82e1e699a1d73d1a0b095a7fc4433bc6a8baf647f536b1aa1cba93d7a2 +size 64152 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..681bf837 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c77673170a7400d3373588aafce23fdccf1f82ce764c7b90cfa2937a2c44d4a +size 45994 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..ac36f1f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb177a644a80b648ac90ff921e73466b94a3113ad5a48908455b62083e2941de +size 19046 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..71efa171 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d997587325ac07cdf75211b865429ac0e694ea79dc4f4515d7eb8db6d0a6a04 +size 46623 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..1c6bb6fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e4d2d532ed1b58509ed7f4392ea91fb5fd0d14ad251b183a41bbc7ad1e32708 +size 51432 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..2b2c561e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:081d7c3f1add18be228ea7109eb92b0fc9ac8e07a449f2e72d713ab5e1030fe5 +size 21591 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..2c77743b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c4ac0598131001037d011a76c5515a04437fedbb9690bf785d17f27c8029f41 +size 52044 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..d674a607 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38cc5f26c8d994822596d68a05bad5d9daf7e10bcf6ffe0b8f5ef6f460e468a5 +size 61383 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..85202a15 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:297956c382b72875e003eecafc860cb44df019c63752df599841e93c7e039aee +size 29719 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..7e36b24d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:867e39beb9d086145549cfe7f26370b0c6498e0449dbcf3a873dc9be3681be01 +size 62282 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..57107f01 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73a6157fc3ed65e2bf6f04d67433855fe6717aeb38a5e22c8f5793e72abcf265 +size 61098 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..26a78419 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ec054101a2f9ed8152e863e60d745a616dbc533cdcc5bbef8828a1550f30728 +size 24657 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..7c0cdd2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a14972664a55afbb224cf76e27190f0d68e335d60d5ed67d865f1de32bd9bb7 +size 62155 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..a8410f0b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c3241a73f3a1edd81868ad9843b2a28f73ac5cd4924580781673ac563e166e6 +size 64964 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..2ef8957e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21309ae0b17e3e5f4f55b2cdc8a53029b2e7bbfe7d6e65492eaae45c43ae907d +size 33907 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..11dc3754 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85ff94d0ba8e9d67439715155687a3996c54e3158832a02fdcd238c3785cfa9e +size 66513 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..036a8c20 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b24fe43bec0695654e2c25c41a7e64890d89222a52608f4c5220f600c268d47 +size 58139 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..fb6cf7a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:329d324414b962cedf6d2d34acba6e4c820abec06cc5287f76813083301d38b2 +size 24480 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..8a2af171 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1acae58c8259fe54deb34663dbaef68b98745f75154a003d3834321c768cac7f +size 59297 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png new file mode 100644 index 00000000..f966a11d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_authenticationmethod.none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:325432feaa1bb12aee7ddd3a1952dbbf131d818d495a771b1c7ce66aa42d409f +size 62710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png new file mode 100644 index 00000000..8252d0ed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_authenticationmethod.alternative.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fef2ea09408188df6d886812686b23a8d86cd30751f73a5070faab3c4d63198 +size 33493 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png new file mode 100644 index 00000000..633cabdb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.cardwall.ui.screens_CardWallSaveCredentialsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_authenticationmethod.healthcard.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f930919f60049e0decbf2005d397b5a2017657ccd2088850b0fa0c20b891893 +size 64126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..aae6699a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73f4baa1c1c4f2279f5e9a5faa89221687c2b7cc6266f62658342901742a7c4a +size 16776 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..a4616aee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:435246cf0c51b7da07d43c2348ea77e6efa8c4dc0dea62b39c0300447a30f971 +size 25576 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png new file mode 100644 index 00000000..a39a8181 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e444ebc84378d3e1d1161270aaa905202155c1fb49ef32c75e3885181a15b92 +size 56480 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png new file mode 100644 index 00000000..d4af84ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8b6f4e8d2445dfc7236214bd270023ef81b473135c15c12bf1c1a044434a4c9 +size 15899 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..edd081a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85a07d5169ad9aa51503b6db2baab1623901d2b29436bfe4d20a305fb88388ed +size 16622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..4ce57a84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cac2b18a87ceee30244d4921b9599b47e81114569283db2cef1279fd1b9bfa3c +size 25289 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png new file mode 100644 index 00000000..05186d6e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dd3ff1285ca0861ab90b86eacc5db94e7cc0b9a29b32269abaded3f83cf6152 +size 58534 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..66f1757e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab61237484e356deac5745717ddf4ca999ae3d2b07fda7c1d32f8119db68becf +size 18128 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..b1538720 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09b32bb0fa13e927829598b87ed9862233a6ff9848795f513b9f07d70a3e6370 +size 20826 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..a95100b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b883ac2b83314106276ac3fd08d6cd2eac72d8deaebeff841062da7742c52d8e +size 33091 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..5fafeb61 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a9fb31f83e75c1d903b63cca8d354f71b77a337008b417f77f89ef66cc976b0 +size 67875 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..9a7bc4a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2720f56d7774c1a2f75061c91e713754ebaf3a99bdbdc42ff31a5682145385e2 +size 27750 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..b2ac0ea4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13bb509f2ad5c7c195c6a95c72b084e18472df4c3aeaf349c394b6b9f0ccadd1 +size 16505 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..bae4dce0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98c85ead982d76db2bbfd881dd16f634c70a6f0be4fdadae82b48cea71fec4c2 +size 24876 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png new file mode 100644 index 00000000..022950f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91f336e2f6ec3d96c5c114b31347429a5b5257674dd25604e7bce5957b21ae53 +size 52423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png new file mode 100644 index 00000000..9e428322 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:488e1ac7c40c6000cee66402efa6f05630c1f91f6ab339aaf1d69a0e14e7efd2 +size 15489 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..974b50c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56b516939e783b6b902b83a164a1ae824bd95b52fdd1020572a256a94c7a511 +size 16345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..800d83f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23a8e8c32fc56f10678f6ccaeb5b6023f288003809649db8288d31f11ed8f885 +size 24657 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png new file mode 100644 index 00000000..89ec7fcd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:381eb63e5dde8aa1ce6a8abbc7c0a97588bd9b4136e9e690e0507e29dfe2490c +size 54364 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..38e0c9d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a3b6c41c8150df438f4f84dcde9ac03c849f52b73580559400bfb421aec5cb +size 17349 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..33c249f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd48359883d18a9b62a41b9ea83f2c5dbd3cbe473298a34e2249ed57d77c5b47 +size 20449 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..3821db63 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be3f4f398f9cac189ba403c373725e1fced1d5c7b2a56082a0c979919719726f +size 32073 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..7b4005a2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5b9dd136682e454353c9d3538e501cf60939af79e7f03bb21590d4ba64c06c8 +size 63765 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..81a0d9d9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413219f454804621e4e53b24c3646af2605c2ab2e2d0a9367d0932f89ad872a0 +size 26572 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..f87becc2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:989a1183fabd160e6927b87bc64d0ce348897ca7eedf67618f5979c219033049 +size 18909 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..da2ba391 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbfb7ef736554927d9c24a4987fff4e03b9b754a642563e42917c7831588c3e2 +size 29214 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png new file mode 100644 index 00000000..6d4f77bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90f615d299ff9f8b35103835bd93386bcdc2c87b8365bf66ad47b5c53a088331 +size 65245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..d927d550 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4cd5bb8c7d59bfd908eb90308afd566b01366d4fc4cbb4c79d03b51fb3f8e59 +size 20214 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..0be18d2a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06bb7bfc46ffc9dc590b73c3e5cead04df9f41a92d3fa0e087d03086bd9fb524 +size 23295 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..99384260 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c5358a143f2d9c1b66ecdf036a6f78889b57895d6c593bac769fa9a0c5d3951 +size 37961 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..47c1f190 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3db059a665ba7d1c558bed6144274e770d894031c1d050cd2602701d96335de +size 75316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..299867dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb5ab6ca19c5c1e8786af5a8779c1a84e015797639283b2ed34a605ea768cff +size 32216 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..0134c37e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51426c76bf3014c604df44828eb1f285d0417c1d31249ca1fddd3161eb0f55ee +size 18451 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..99d4562e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fc6c396fad2fde49f1e21ff9d3b0564725516f1a1801dc82d754e072eba7120 +size 28195 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png new file mode 100644 index 00000000..388d4dd1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59d724960015a1a242a8d560887ff9d7aa63e16c76e8f9b7e8d2698f8f8433a9 +size 59721 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..2d14e1e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a6710055daf28a2022bf65b86efc5a39c8af3e429a5d5bbf8ea20d018c0f338 +size 19453 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..1ed5ef64 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b31fd034581eebd70bbf306fbe4409c2fc631b8648b08d95a78e4e649a4cdf53 +size 22931 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..e86bec12 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aa473df3cad7827d4c6bf50cad7a266ff8ecd8c75e412e58609e2a294cc324a +size 36814 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..0248eb9f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b80743e62c751d55a585f5c99088adf3b41ee6fcd84bf623382968b894e51b8 +size 70752 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..44eafe68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationListScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bbe5c1014dcea465fd70a0b99f33b1adf25c5544d89c6f08db5ceca4bbac63e +size 30402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..ee5489a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44c40696215c647fd9d4464ea8ab028f1aa3c4f0dda7175f1490838fdeb620b1 +size 30126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..d1c902f5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e5dcda492148d7e5b9952e806d58bd55aff03979ceae7b63635ea2d67cca4cd +size 52963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png new file mode 100644 index 00000000..a3556a59 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd746866b2e05f3ebc1aff5a86ecd73f5e82c1afe13c064e47419d1407a4c1ff +size 22781 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png new file mode 100644 index 00000000..5ae3b428 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d0896682eb1785e3eae6f92a663859e38b846bb12eacf6c246cf14202200886 +size 20026 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..3858e2ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea2dcb3931374f6a81bcc18a597f10b7d9ae65cc7c11ef682755b088832536be +size 29708 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..a295184b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e162bf51447543b8e5603a3c4d6505e862907cbc4bf106cedd9d29e208eaaa35 +size 52396 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png new file mode 100644 index 00000000..37a32ac6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6c8ea45cae4488100c48d8d51549a22f5f9255c823a709654abef39214fec55 +size 22536 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..9c24afbd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46cfb96debf2d0eec9e54edf5d4b01cd266695299de45f48f97d878f0e8cf8a0 +size 22307 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..f029b568 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f67fbdf3623ed8ae43420c5e5e6efccb17412e9f8573318cf7ad5467b3939b +size 38619 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..60b60c96 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6439121a3ef63a30d2ba9416334a5e21188643563548d7f5df8096f17739bdb7 +size 69935 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..d12170ef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9db091e158465c14e5bcc21be35ad69ebcf02b5a355aa5b3c835ed5ee90b27a5 +size 28422 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..5b71e964 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e12dcad89b767b2da819156c8651a1c3d6ac0f08481559c65e10e61b0d41e91f +size 29962 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..f2b35c4b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a87b9f457a6143efd82a4f08a7e5ad4d8e4eed5667cef2c849270e3d3193ff7 +size 29010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..6864c62e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ce82a19df0f6fb0969d5ee396d1f4fa9beadf4ecb9653487bdb2838f7697fa3 +size 50473 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png new file mode 100644 index 00000000..0d3b1041 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27c161a83103321c704f6ab6c073d2af08be84687bb26b7fb99be0e4467be1bc +size 22435 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png new file mode 100644 index 00000000..0ee2fcd9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:715ed0d345c2ac0fa4b43895bc8dac63c726a8f620aff5ddcff59240347b52ff +size 19595 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..ced8cf4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d12ffc89ad3f92e0a5a2a21fd3e2953e7c58cb98736af0bf86c01f956976d15e +size 28642 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..65a1938d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dbceff7081526396405adf66c00c05a73a38e31106195e54dd0c333953976d0 +size 49947 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png new file mode 100644 index 00000000..1a200672 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2924445282eebd8430f26ed4621e46640fe8f7ef8b75e3cd76d3c2037691dd5 +size 22221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..86ec6e14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f0b9538dd0a2b1741e4b3058add0f322503f3aa3357e7e7cda5240fc01541bb +size 21521 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..4b88404c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61328081ae734ee8b8a0146c31d3b185244c4659464020df25c8c9eddbb405cf +size 36278 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..f659e276 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a281a3bf1c3f1841587ba262d9293cdffecafd19ecfa55512e9fc7ec6f167cf +size 65843 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..43930e02 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a6d96cae81221ec89efbac7d5323bef28782dbd15095614f5a7e992532bb1b4 +size 28081 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..0fe9a011 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da53e68b6678add1ae3643fcf334713059a0c963a2245afac195bbfa99b3ab71 +size 28893 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..69c08f61 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9845e39c8d6c07b55eabf376fecd71c2a7ca136c1fd0c95877eca7db72438625 +size 33856 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..69bd43b9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e0c81e1696f58931663cdb0b4fd437dd58d1a706828bee13ac279c51a87d399 +size 61016 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png new file mode 100644 index 00000000..1e546522 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec47ce7947c2795e554e002fde1c047c6e164b71b40c6abd47c492c12521512c +size 25652 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..406cea52 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e8799cfe041241d22798d770f0804c3afce9e0d19ef92faa353d59a4c0cb1f +size 24892 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..92fc59cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0144aa8fd5b25ef40770892793e5b5002850433c702a1801a70579152ac34664 +size 44481 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..e7630036 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4ea66536392c8ce8c9115b28ea5d1bfa1b22892b447ae28d0fb1f9adca46e05 +size 78152 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..9524495f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a5954181378974632fc0e59ce7dc6d7b26fe4e9be68bc1ef14fd3ddd4abe48 +size 33167 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..d54c92bf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cdf47ac45bac1615034ca95eb69c2d1f1ea5a230d90cf051fc7374f8c44eeb1 +size 34868 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..31b1849d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ff82a5bd7eef3f2f9b830c1f160be34556ddb23ba65569a890dea81f96b2d87 +size 32112 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..06c18002 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d92f3a360c6275acda5f5e549435a9e83b440289e03d79affe88e30303d3223 +size 57243 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png new file mode 100644 index 00000000..be7089e8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9a2663f7b2993a91565055c904fa092d9e77cff73ddf2a18fd67dcf2184f27d +size 25164 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..a950932c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:968095c49126af31ced9f54ceca8f450a9160de8a5cd020b12c25108d57d957c +size 24134 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png new file mode 100644 index 00000000..42094c09 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_one_profile_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa98800286793f480853e4ad3be50258da93efd451f36db69a16d3f6a80dbcc4 +size 41925 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png new file mode 100644 index 00000000..b8e26b43 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_data_state_two_profiles_with_notifications.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1de2636b145d6a596e71fc9ad2cc332c95661093f5565d3199de238889633e50 +size 73518 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png new file mode 100644 index 00000000..ea5e77a0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95f49b0c4508d382b94de32155aa3d12a2c182d212ad2d9bc6d741b5601a0a11 +size 32745 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..f226e62c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationNotificationSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:978a08569bae474310517e17cb8449de9f41933a11873a7cbf66f2d6693ad927 +size 33184 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_dosage_instruction.png new file mode 100644 index 00000000..c6b11fb3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84c0cb26f6b5687f8b98702bfca47b45377cb7319002fee02089f0a3673b24b9 +size 35182 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_external_dosage_instruction.png new file mode 100644 index 00000000..99c9aac0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b35a02580aee9fdccfce93e6dd2a9f83c055af11ac4c73e5461b07269b1b5ffd +size 53335 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_free_text_dosage_instruction.png new file mode 100644 index 00000000..5107e814 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:359d9fdd203d3b7cecdc357719da747d58e67084d35bda1b206a6c92d0d4cb61 +size 36731 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..0c935738 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6ff93a4c9740f34ee41de03e6b8a44ebea5ae66570477901f6b8c94587823c8 +size 40730 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..9d22462e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a13e8fcc2532ad2cea334fd7c1cb137aea5b8fe6cf4a4272edba3563717ae20f +size 52189 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..8c801135 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a908eab66d880ecf1a14e47c65e0c0f0b85b1a4e1f1b5722bd1e49114b5ea09 +size 63850 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png new file mode 100644 index 00000000..f9fdc0bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cdaea32ab995e134dbfac26ac760ccb2f1ac4925eb67049e0e1336accc9c3b5 +size 29284 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_external_dosage_instruction.png new file mode 100644 index 00000000..e9a5ea4c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0b54c0893b607728621a519afaddb203061e5374eab89cac09bed587ac1f5a0 +size 54678 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png new file mode 100644 index 00000000..b2963046 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d50e45461e16127046e0f6a35a1e05ea1fa0d75d242dd83d5b4865f6899d58d +size 38946 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..817da3ca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f91453d6c2de8825f4d90cb9a1814059407b43dcec1e6089fb48dcb65bcfbe38 +size 50493 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..c9a94f36 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34425b996add46639e8b7e2e5cc571fa513c3787b488002ccef94d5b46e7257c +size 63844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..8c801135 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a908eab66d880ecf1a14e47c65e0c0f0b85b1a4e1f1b5722bd1e49114b5ea09 +size 63850 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png new file mode 100644 index 00000000..61e0fef2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c7b91101cdedeb6f2f222ab1f3865f6b986f35269ce4aa5f2b843b2afd426e +size 55178 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png new file mode 100644 index 00000000..a0f4e97b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:149d947c7427642a5ff13433d6f2b74cb53633c82f0dcb66d310dcf854251dda +size 90804 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png new file mode 100644 index 00000000..c5529a20 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf9fadcb8c03e7ca74a4ac7a73517ff8125cbd345798409257ae4ac6fc015d2 +size 75279 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..42601d85 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eefca3f65f30fa68ab7d5767018fb7108f2b75c2ec465fdcf7cc2b9bc3f63a7a +size 93078 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..95b4be6f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e69581112e798551a06a87016a33ae24c0e07fc34f01b9fd98f1ffc2f00a705 +size 87771 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..6f0f2d2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0964d82e7ab49dc4bce0421916940f366d5a2d709da71e5fe4b24938b66fc67d +size 85099 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_dosage_instruction.png new file mode 100644 index 00000000..33ef3137 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efd663ea9342adc9ba7bc7e64e546b741e32ca5aee3f417f863a5cc0d4b92076 +size 31480 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_external_dosage_instruction.png new file mode 100644 index 00000000..f95d84c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24c202778cd62fbcfc092f76615540f7a500c8ccaa1a711f3406984f48918f5d +size 47936 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_free_text_dosage_instruction.png new file mode 100644 index 00000000..456eef7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f845e1405a6849f9bc4102c28d6126f69faeca9cb28d0d6f46ec6af7dc2a0dd +size 33010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..9944c46b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:411e0f3ab8bbcc5ebf9bf8955143f9a906d815ca448ce0e7015be82b4388dfa5 +size 36675 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..eda47181 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abcfbac68dcc31fca855a28df86ed021f49c19b47b47d9efb49f56240308fe8a +size 46812 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..4adfcb7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf128afe9da1b333f5335fa4bab3872c24ea574929abb2c563f8aaededecf3c +size 57468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png new file mode 100644 index 00000000..25956a7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca572bef433a209ab02fbd2dea43fa707af5ef58212a9acc735cec4e5c4d0e +size 26210 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png new file mode 100644 index 00000000..232a061c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7622f249e2c5c5ec7bf99f54a8f95bd86f04e781840c61c8da01282583bc83e +size 49347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png new file mode 100644 index 00000000..d2dc014e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3a3c5e335ec35b1d08fafc9f79b844f3433c83725cadcdc6481bea0714283a9 +size 34903 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..ec98d5a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65a523cdb07ed89a8d87a5b9a7f07a60c00c2b79fb3c624b1e20695c03b92b89 +size 45336 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..fd3bd3d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ff166222790a5e3da1a34c1338171181fa0bb73a854af2d0653dc551a1afc97 +size 57341 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..4adfcb7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf128afe9da1b333f5335fa4bab3872c24ea574929abb2c563f8aaededecf3c +size 57468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png new file mode 100644 index 00000000..44cfbb5c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f9edb805920f22a04c333b54619d44c538b4d7b562cae0877e6252b50eff88a +size 50368 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png new file mode 100644 index 00000000..b5113ac4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a131cb530d2c73858d1e94c58ae8620484e2a1f99eb9253bed80865bbff5b771 +size 81788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png new file mode 100644 index 00000000..9dcf4f65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1764e1bb9169d0a3839d275210c46c02afe4ec89c4324a4619ac28455ca20d72 +size 68035 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..fe687e59 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7cb3a374d050deefb8242fef0e34ca846cc85cc37b19173bf07b537d8594f2 +size 83277 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..f2f3fa8f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2c9d64c5474861d5c0b8e9efa5f13f44a82e9853ed994f1a16fbdd8b502252 +size 77357 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..c13b612f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bda4f0edbd00285a69116c4403e595ca1354175b9ae301e538f71e6c64c9c06f +size 75376 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png new file mode 100644 index 00000000..48ac5516 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddadd09b140bff9040b8315ea4e75562e046ff80881cbbbc736b67beddab0c8d +size 35110 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_external_dosage_instruction.png new file mode 100644 index 00000000..142fbde2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:133c713f2ec96f5e73ffa0977e6b85a75bee102c8c7986577ecfc91fd849fcee +size 64978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png new file mode 100644 index 00000000..2a666f4f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f84d7efc6607302ac057b577e0c216a162fc36a25794c3b86a56f25f9cfe53a +size 48463 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..efe1a6dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4016b997c56ba7a001da460632feb55c8fe0024cfb74aa1138356bf83e02b37 +size 59737 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..65ae5ef5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4871b9bb4ff03eb2d44e78d19af3096a699759ea5fae21605bd8266c02494aa +size 75892 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..2837666a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7b241d3613addd9a982b883f3d5783cc32a2b6ebd89e8b8263b443fbaa79271 +size 75944 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png new file mode 100644 index 00000000..76bc5786 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29ffe7891a52dab1ffb66ae5f50326758b78537e4b509231a13ed5a64c5e6f8c +size 66801 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png new file mode 100644 index 00000000..4307646d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4dd0d903dfac11dc43568de44acf2f9482a217cfda099eda6aef90e08f929a8 +size 101572 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png new file mode 100644 index 00000000..a996141d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a292ccfa765533b8f54eaaf80b27aab0510dfdbf96c396188c9411b9e9b797 +size 91018 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..2d4a3496 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396f4d4ddacefdc7b1677e6232a0f032e06b9d94cd57a534052b343baf655699 +size 98050 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..9a6484f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08e5def98e4882dd29448973de19ae723d251878d893375feb272e0b383bfafc +size 80641 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..879912fe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:207193cfd23176b3d9ba7211203178bab2b35bfa7ec5d1ee77d5fe2b33020eb8 +size 78394 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png new file mode 100644 index 00000000..84c6b2dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18e74b4b23d8197ac3760fe6da13eb1be2f335e884acc38e426ecdc77c7ab624 +size 31329 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png new file mode 100644 index 00000000..04374b9e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa3a6e4e287f0d9e655a89d76ef5fa5d49ba7eb09d772c64b55560c8d6d39e56 +size 58123 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png new file mode 100644 index 00000000..e654fe1f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ae4f1c92e18b2bb879a252779bc6dd0f1ad216ce2ee3a44c557e7cc37704edb +size 43482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..58e9ca03 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d23a9fb01130982c6ac0ed0f88bc6bc87d93083443d7c5fd3c7acb5fd728da1 +size 54381 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..984bc64c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e261d654bdf872c4e7f94345d4c79bfdd25f34fb014af738d98d62e37b16992a +size 68269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..28d2e250 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d28eab65b1ba336bdf759a485c44a2a718674254f8f5b45714a773b6702511ce +size 68451 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png new file mode 100644 index 00000000..83df2f7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_empty_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef03c4569c3a00db4f64767987f81beab254a27d22f19ae2db9105581c4c4e58 +size 59732 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png new file mode 100644 index 00000000..ec731481 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_external_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:946cba1859f06bb9e8efa4d9104873a8f20a60ac05b183e6b5898a5517f43c69 +size 90142 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png new file mode 100644 index 00000000..f51a7978 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_free_text_dosage_instruction.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a057be1df2c8bdeb2e747153d3ddb8d5554957530809fe2f8ccf72b176779b4 +size 82262 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png new file mode 100644 index 00000000..6c6d22a9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_one_in_morning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:010baa8d81a035b29c979e433debdeb5ebe965b8ea29bb937266bb42d2298087 +size 87610 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png new file mode 100644 index 00000000..0e755829 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_day_times.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88b5cd1221da98261c3870232a247162b762197dd55bc54438c127c8df1da65a +size 71352 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png new file mode 100644 index 00000000..68980289 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanDosageInfoBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_structured_dosage_instruction_two_in_all_daytimes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:246cd65c8b29cbc3d8e369bffa285d20ae77876ebe4281720328fcc41372307f +size 69738 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png new file mode 100644 index 00000000..d4af84ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8b6f4e8d2445dfc7236214bd270023ef81b473135c15c12bf1c1a044434a4c9 +size 15899 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..c83c9f32 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57c86870d8a2b280e1edcde17442119262ae84447b098911ab80e228951143ed +size 30269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..8c639b96 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8efee9364c5e4469af0b76704868fc10a49fa5709edfe41bf85500891860387 +size 21914 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..8d700519 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:730fc87a77553c3e5ebb0c85cd60b3ec750adfe0162531c75269f0f17ec1299c +size 37172 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..15d74d60 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0870a4e0de2030649bb5b761f4d4d2834083dacb218302888b520e3ac23ab9 +size 34921 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..75743895 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d771992fdf3fc93e35a6184de749a37c554c511b14875baa26406f161363c5d +size 34599 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..66f1757e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab61237484e356deac5745717ddf4ca999ae3d2b07fda7c1d32f8119db68becf +size 18128 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..e8327e8e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81b668382d3842f625af60983777e22d606c9da4473566e3b4422e0f2c24b6c7 +size 32013 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..9e52245a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98442c286c3c52581063a679238240e8cfb12747f075882e85eb50fd3d6e3631 +size 21666 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..f593edab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d772ad24051d5e1441611b91c665ef84c1d9252424cd90adbecc94cf48ffd2a1 +size 39329 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..f8c6c9e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e06f4013c36c049b824c77a7e9819d9c839dedcce97fa8ca918a3983ddbb1b9 +size 36522 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..7a34c216 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f67023a2610eeedaef6b5fc8b8de98f35ac1ac5b0ca9685489785e344aa8ed1b +size 36427 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..9a7bc4a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2720f56d7774c1a2f75061c91e713754ebaf3a99bdbdc42ff31a5682145385e2 +size 27750 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..83ff83e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f33c0bfdccaff0e7e8cee8f5b2418380f31f39fb8a5a15b9f012e86e00d59f8 +size 42455 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..bc3df3db --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a13a217d8217fef700a6d4dd0dc4b956f8741eae0430fdd6586079d9a371b5f1 +size 27398 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..b52956ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b5714673d0d2b918ac8cc3c66fadef7899f54c69983d90fcd137d6a68726b30 +size 55789 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..3151faf5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:108076191e15c85b4ea373139fe5f7aafcd29863e9df5f6c74679d33fbfc5b4f +size 49136 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..d58f455c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e3887244a66be4f44a0173b81c5b01d331d26bb20807acba4660c2f51d43c17 +size 48647 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png new file mode 100644 index 00000000..9e428322 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:488e1ac7c40c6000cee66402efa6f05630c1f91f6ab339aaf1d69a0e14e7efd2 +size 15489 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..e888966e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cacca3d9c4cd111ddd0308bd831fc5617ea243039020d9ab62cee857c37e4052 +size 30003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..d5307418 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca166d2137245ed19190d2d7dd4eef188bb1c5d7dd3bf850b9f92c36a36c14bf +size 21726 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..09d7875d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a660b93c9a3aa7fe669bdbf5b9eef0244327a60f2f795e94d4194b616cf66dcc +size 36822 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..a2421d07 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3875a0c9f848dd24cd6f51634baea12c87f6c109e4df16f0209558c93148a9bd +size 34783 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..00b3abed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:694a28f62a2eba7e85c95ce5fcb69d6a3f840ff1dfcb53e25743ac87122838cc +size 34484 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..38e0c9d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a3b6c41c8150df438f4f84dcde9ac03c849f52b73580559400bfb421aec5cb +size 17349 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..7fb78479 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5835da386c6037c36d17244e7e5f01f313bac23727cbcbea851629023d38a77 +size 31616 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..d5bfd101 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25ef32f65f73a27e43c0026fe4234983468214efa4c6aa297ab8f5cf528e41df +size 21510 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..2539b061 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb65b9249704f8b61edd4789dae86e0fe196263fafb1cbb6342159263b663f0 +size 38768 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..85219511 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06722f490f7fa121372d8f8eb1253dbb17f47e77fb7fd0ead76638a622921fb4 +size 36217 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..cca91433 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88a5036d00120dc55f746704859552ba16169db093bce092fb6f8271e91b4c42 +size 36156 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..81a0d9d9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413219f454804621e4e53b24c3646af2605c2ab2e2d0a9367d0932f89ad872a0 +size 26572 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..911a5cd9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84c2bf8e5c8aebffd790b47fc0ff5e95fb55c460fef15ae364cf10eca753b6c9 +size 42051 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..3d98d675 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a5ac16ee51d8201089da0683f988d915dc52b6f92d354eca4e85db1e0961811 +size 27046 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..97546f36 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53fb4b41b440383a75ced142fe19ddfb49393a628ae94cb4deb5004974f8babd +size 54698 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..32417a0e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:237c58c5afea7b712eff9d44673cf506374c1355403a237e24f2b700f66708c9 +size 48411 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..29bfe9d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:019ad14c677147baf59f42d9454a0c414141b4700736f895b6c99b8c2a3c1814 +size 47979 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..d927d550 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4cd5bb8c7d59bfd908eb90308afd566b01366d4fc4cbb4c79d03b51fb3f8e59 +size 20214 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..d1d0f611 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69b1a6032c66eb19f97c3ea37e7b28073e68e0eca6bccbd2a906dbf8819b811c +size 36424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..83e416db --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5d0faa1b15c2f08f79abc7e527540c82e13a93b1fb1879f47b53b5dd5dc982a +size 23988 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..9e58c0a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5104ac48bcd6314b5a49b939561c0817f5549b79b7c72b34cbea5aba61b475e +size 44605 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..40a0d9d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10c75428e4bd2fb68d5a59b1519a4fc5fd8a266e12a4fa83f0e5ecd532f07695 +size 41487 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..f03bf7c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e135d9f5e5a52e80070a9903665c8ecffaec71ea39f73b85ff962643b15ccf0 +size 41304 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..299867dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb5ab6ca19c5c1e8786af5a8779c1a84e015797639283b2ed34a605ea768cff +size 32216 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..9997bf77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1367616ea761fdc5d8bce0755aeac841e2e5dbbe50c068f352fddc2e64436fbd +size 48933 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..c4077cfa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c4e2bf65828c959d51f18e13232a7e6f910e462308651f2e0e126fc2ff00ab9 +size 31821 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..c656ef40 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:596c786696a98352303dcfb6d17bca866e16f85662c9fcde30cd2a82c8091002 +size 56907 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..fe9f6b62 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ffe4a8bd0b86ac1858935666c86ea73635a72dcfab0b819a07bb2b93763a603 +size 56142 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..4d8d65b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa176f83845c0cc122c15530eedf47d0e4fe5647a50b93487166bbc8bcc0d027 +size 55703 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..2d14e1e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a6710055daf28a2022bf65b86efc5a39c8af3e429a5d5bbf8ea20d018c0f338 +size 19453 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..6216e3a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a3e6a2ca59a5b3130879dfdf26bcf620432fabaf6c4bd795adadcea9fc132e9 +size 35487 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..a4534573 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aefcd4c521d80ffd4ab2268bf45860c721b0a5a223ade3cd86c4b46388fd39a +size 23596 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..96886388 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a600630f89282be698b9b4136bffd135eaf84577d25469cb707674a0c671d244 +size 43130 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..564c2888 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6baa3ad8a674266256f98dfb4e9ad9e00cb0162c7ba5d08657145eb05112ebc6 +size 40363 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..4fbb8817 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4f59633a3181606ee384f7a6a6ab9b821e98e465e633d8255b30d7782100a9 +size 40213 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..44eafe68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bbe5c1014dcea465fd70a0b99f33b1adf25c5544d89c6f08db5ceca4bbac63e +size 30402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png new file mode 100644 index 00000000..9f255d3e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0a768428681829026d816ae642025ef603a1782a04026303642cab8f0285fdb +size 47170 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png new file mode 100644 index 00000000..b37f63ed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription_schedule_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c47e2873978a6d90c0b0a655e36ab6f8f7771609173bdc5e35c67b4df39362d9 +size 30353 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png new file mode 100644 index 00000000..2e9cbf3c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f69fa4fc1b949ed7cbbe86964d8bc0b669b709173e3296e168186fe2824c84fa +size 54552 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png new file mode 100644 index 00000000..e422b33e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c655228de6938f002500c179b18ca85e7d374a710b6e2549f9f1a55a282d3af1 +size 53927 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png new file mode 100644 index 00000000..e439f716 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_MedicationPlanScheduleScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_structured_schedule_active_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:656b8d8a7639a609530a2c0aa5878e2ed4776a337d3463b34a9bbd4c806c8f75 +size 53482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png new file mode 100644 index 00000000..d4af84ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8b6f4e8d2445dfc7236214bd270023ef81b473135c15c12bf1c1a044434a4c9 +size 15899 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_endless.png new file mode 100644 index 00000000..445282c6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bd5f70127000d2d09fc287b33dcd9ea6f136c535f0bcedd3a1b4d58067c779f +size 11624 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_individual.png new file mode 100644 index 00000000..19c22f26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:989977cdb1de20932fd4c71158efc2a373e3553e90e1ebdd0bda56440922f49a +size 19174 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..66f1757e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab61237484e356deac5745717ddf4ca999ae3d2b07fda7c1d32f8119db68becf +size 18128 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_endless.png new file mode 100644 index 00000000..df3cafce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:161d061174d66b49edd93cec2b71fe76d7564963db44b47a68a7bc985e8103d0 +size 11647 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_individual.png new file mode 100644 index 00000000..00c05785 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f75c046954012bad8d7af91f19167127cbb9c68a069cf4621ae4c89e422d23ce +size 19510 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..9a7bc4a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2720f56d7774c1a2f75061c91e713754ebaf3a99bdbdc42ff31a5682145385e2 +size 27750 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png new file mode 100644 index 00000000..e23fc220 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3503c138a26caf67df7e763685af267f048cd3aaaa7a33a980d0bc12452ef99 +size 14819 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png new file mode 100644 index 00000000..e4f99383 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afc59225066497264d78accd079c919d24b690fa4deb85bd00f47ebcf2cd147f +size 26944 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png new file mode 100644 index 00000000..9e428322 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:488e1ac7c40c6000cee66402efa6f05630c1f91f6ab339aaf1d69a0e14e7efd2 +size 15489 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_endless.png new file mode 100644 index 00000000..d84c1fbf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7f70171e2512a895d907c562d05291440355d5570448bd24cdc3a65e12faad7 +size 11534 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_individual.png new file mode 100644 index 00000000..396e7303 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad494acd07b03a3ded45d38d4b3d5b6d7182d88bc8fe6737f8adf4636e5794f1 +size 18913 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..38e0c9d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a3b6c41c8150df438f4f84dcde9ac03c849f52b73580559400bfb421aec5cb +size 17349 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_endless.png new file mode 100644 index 00000000..2465019d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:315c296f7641d1581f4e4b232c97de445327b7de7a39a92e9712edc7700f7f61 +size 11566 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_individual.png new file mode 100644 index 00000000..ddc5234b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2de3465e70792f746808eee89a61e1ea9b366fe01f299514000de33475c60702 +size 19251 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..81a0d9d9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413219f454804621e4e53b24c3646af2605c2ab2e2d0a9367d0932f89ad872a0 +size 26572 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png new file mode 100644 index 00000000..a7455f37 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04c1662b15e0188d5a8d9c2c6517ea664ed3f1b3e22947672f877c8ffef77283 +size 14705 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png new file mode 100644 index 00000000..8881ed14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bd205accdc2cc4d85823dd28d260983ae14e1423ca35b630e701c083331f82 +size 26814 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png new file mode 100644 index 00000000..d927d550 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4cd5bb8c7d59bfd908eb90308afd566b01366d4fc4cbb4c79d03b51fb3f8e59 +size 20214 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_endless.png new file mode 100644 index 00000000..220b309e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59679a723f6e0856a0a75dffa11f8487be06ac98e4f450c37b50875bb391cb2e +size 13208 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_individual.png new file mode 100644 index 00000000..d0f56007 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b13ace2a34a6e2843f9a0a13e1bf34bd973642807528a3a4fb821ed4c1e05bc7 +size 22130 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..299867dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb5ab6ca19c5c1e8786af5a8779c1a84e015797639283b2ed34a605ea768cff +size 32216 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png new file mode 100644 index 00000000..363dd351 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ed39dffcc13263c84a2a4898efe07805ba289430f71b6066a213c8984d190b4 +size 16804 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png new file mode 100644 index 00000000..60e263e5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7a7403613649f4871e307b72ed8549211ffcaab22d20ac8621cc007db27f530 +size 31206 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png new file mode 100644 index 00000000..2d14e1e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a6710055daf28a2022bf65b86efc5a39c8af3e429a5d5bbf8ea20d018c0f338 +size 19453 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_endless.png new file mode 100644 index 00000000..e6217f8a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74460ac06efbb5cabeb31db9f754cadb245dbf34193188b0ca28e70f76f2e449 +size 12931 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_individual.png new file mode 100644 index 00000000..92c0a9f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74280affd083cf12a5474f57ad104efde482b6a34877f8f4431ccbbc4ee83b99 +size 21590 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png new file mode 100644 index 00000000..44eafe68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_error_state.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bbe5c1014dcea465fd70a0b99f33b1adf25c5544d89c6f08db5ceca4bbac63e +size 30402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png new file mode 100644 index 00000000..903e9576 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_endless.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53785bb3464670004277f144d1111bb95c1f712576d41ca22eb7ca421b42cb49 +size 16533 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png new file mode 100644 index 00000000..b535159e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.medicationplan.ui_ScheduleDateRangeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scheduled_individual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bb4aad21436da00f59667c34f4836d6b154ac0af22ac5cccb9516e2afd32b8 +size 30502 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..4f6bd271 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0cad7b19c89d95b4b04ef64d4abad33892dd5d210b4fbfbc730fc8db86e2f90 +size 21347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..7220a811 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:372d360bb3f8b6e006b42a65ed73e291c8ee0650ada7aba17b21f462b3750c71 +size 26173 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..8b1a0ebe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2c57ae3674984a9d3bfd573ee40d39c7d5f4d7b150246442bedf28b752a5883 +size 20517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_6.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_ENGLISH]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..4f6bd271 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0cad7b19c89d95b4b04ef64d4abad33892dd5d210b4fbfbc730fc8db86e2f90 +size 21347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..7220a811 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:372d360bb3f8b6e006b42a65ed73e291c8ee0650ada7aba17b21f462b3750c71 +size 26173 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..8b1a0ebe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2c57ae3674984a9d3bfd573ee40d39c7d5f4d7b150246442bedf28b752a5883 +size 20517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_6.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[DARK_GERMAN]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..0d7d505b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74cd6d3a6373e0ed57ee21c8ebd103cd1c67c6a2f1fffa32c1feb7156d2de623 +size 19941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..1520eb5e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23db5a2787c9547dafd1c7679ea13c74eccf3682cf141eda2cbd6281ce772b96 +size 23949 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..d109c83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbc629b8499eb403388debf0796b57274ac14ba7f6c1afe4ee7620114d750d7 +size 19062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..0d7d505b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74cd6d3a6373e0ed57ee21c8ebd103cd1c67c6a2f1fffa32c1feb7156d2de623 +size 19941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..1520eb5e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23db5a2787c9547dafd1c7679ea13c74eccf3682cf141eda2cbd6281ce772b96 +size 23949 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..d109c83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbc629b8499eb403388debf0796b57274ac14ba7f6c1afe4ee7620114d750d7 +size 19062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentScreenshotTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..4f6bd271 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0cad7b19c89d95b4b04ef64d4abad33892dd5d210b4fbfbc730fc8db86e2f90 +size 21347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..66bf320f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31f0f83f65385ef46b33220f33efe06bad397e6dedfb81de16b6f248a1da8f84 +size 25766 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..8b1a0ebe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2c57ae3674984a9d3bfd573ee40d39c7d5f4d7b150246442bedf28b752a5883 +size 20517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_6.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..4f6bd271 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0cad7b19c89d95b4b04ef64d4abad33892dd5d210b4fbfbc730fc8db86e2f90 +size 21347 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..66bf320f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31f0f83f65385ef46b33220f33efe06bad397e6dedfb81de16b6f248a1da8f84 +size 25766 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..8b1a0ebe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2c57ae3674984a9d3bfd573ee40d39c7d5f4d7b150246442bedf28b752a5883 +size 20517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_6.png new file mode 100644 index 00000000..decb3971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76e11c57dd427d7d89484d87d7d63e34fe55204a4e4030c298f5e5098e1ccaf4 +size 29424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..0d7d505b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74cd6d3a6373e0ed57ee21c8ebd103cd1c67c6a2f1fffa32c1feb7156d2de623 +size 19941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..dc6cbb14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f87116c3d2f234babd3b25ff60e90df33d72622ee6baac19a7baf41f25cfac +size 23535 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..d109c83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbc629b8499eb403388debf0796b57274ac14ba7f6c1afe4ee7620114d750d7 +size 19062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..0d7d505b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74cd6d3a6373e0ed57ee21c8ebd103cd1c67c6a2f1fffa32c1feb7156d2de623 +size 19941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..dc6cbb14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f87116c3d2f234babd3b25ff60e90df33d72622ee6baac19a7baf41f25cfac +size 23535 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..d109c83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fbc629b8499eb403388debf0796b57274ac14ba7f6c1afe4ee7620114d750d7 +size 19062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png new file mode 100644 index 00000000..5380ae42 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.components_MessageSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cae7e148fc4ba6c72c1fb377c32530b0feb821c075f2ef36277aef5e721e12 +size 27045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png new file mode 100644 index 00000000..a0effeef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ca358164633910881d794834dd3443163f6668d60e4b4eb85bc509d2f62fc03 +size 11634 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png new file mode 100644 index 00000000..2b24aefa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab5d494e1fa9d84bbc645f0b4fc18ce6a81148ccd9c9b8fcb14c49887bbb5d29 +size 61250 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png new file mode 100644 index 00000000..09450863 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4b695bce5d1b87c661501998bb5795a4be255c77458e5eee07db110efa382ce +size 16453 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png new file mode 100644 index 00000000..39acc5c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f0b5fc5ebad4f742135adc72fb3f1da0e30b57970b946e54b29e81246c6b409 +size 55711 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..fed586b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb1b8518d31f6db5a195794b66e462d6175b6c9271ede93088331c3b1fd662e +size 11516 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..edc34f1b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea848a6fb3ace426af9f010515bdc387c363fbad2a3eb65933c068660ad00772 +size 61995 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..ac930356 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63863920872938fac3e61f51b1ccb0b887fbdb22910c6c436cd0903b01849688 +size 19490 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..19da34c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20334af19182b50b9be8084cd56a2ceb8465c2bfefb30db4ddeaca27e19e53e0 +size 54644 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..363b6ca0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7309c4052b0728c4aba75014bc5ad70da032523be3496ad30583fff37a2c5e +size 12528 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..b680b79e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe33e18c98d881cc817e224b69d15b8c4eb47e721ca3c425e2d173add37fffca +size 69711 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..ac6e908e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc5c97d91c9ea3eab48383678bf7b38d3031df19cc0e0921ee556a158cbfcf16 +size 30933 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..ae08193b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:250436ebd538a7d37a9df2bc64f5c6ecebb58922c17a6f6a0ef2a4a23ac82498 +size 65490 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png new file mode 100644 index 00000000..5cfc3737 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad2cf83e3febf09c7cbf0dca616f347dc0015cc5a7ea482981cfb9cd6096441a +size 19702 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png new file mode 100644 index 00000000..a44a2c33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:798908e94e6d99fb8a4254975f9f82c57b67e5f7d5004211611f445919595be9 +size 57558 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png new file mode 100644 index 00000000..9325a65f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6a34c89658e31eb491ba5b2f3cc789d8a5741be284d0d8088f9cfe9eb50caae +size 15997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png new file mode 100644 index 00000000..f7b9c436 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18de890218cccf0d84fb138a6f5af5476c259da0ecf37daafaa13430d2d118b3 +size 53139 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..a0bc431a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f38891a7722495220186835faa45bd9a052a0af1e1ff91a114b14d9499e6aa4 +size 19539 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..2986fea4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33dce221774de26e4c3790940f39c9fd52d45eca02567fa102f0a158ddde2eeb +size 58303 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..e330b8be --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f515d3d233500d44ccb5fe07298ee0635746cadd6d70c69409f4d77ed517a68 +size 18700 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..47a64a44 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2c31dd76580d738b5fd8a90f0b83784d8a8bab56d21f1401be0b595d781beb0 +size 51933 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..d1d11bda --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286c1513b7bea8583dbe1cef10097afa68e5ed6bd49c44b3f667c318afcf56aa +size 20528 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..e8301b91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df5bac23e2e9d1e593cf05e29c16067f078649e5aab1c7bcc0a09108505a3ed6 +size 65696 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..17d89210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b3a67ad7e48c8354a1cd979c77a637b5e6ae20c6e08bfc185acd00a067ff234 +size 29730 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..46c6bb17 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e23dcbe8be8f18b781e680b1fb7cb90eed81e2ce55c711cc826c0be84815d477 +size 62262 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..1d252ea1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3431b7a23eb3c6ad2279242af60a6f1c3a2c34065af0030c989643abd08d86e4 +size 12109 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..53e0bb09 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffacd3f957711d2d4af15df76ae60fe59be7cc659351d07694be84873dab6fe4 +size 69428 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..76b3149b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:213e84b7fe20bda67c12f5a5f01042aa650a844c06210942f3919c5055840f4d +size 22152 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..32163dd2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9be57f9e1531e1af3552b6f3b12a1e18acc50728c51d5fc4e29a8e90bcd80b55 +size 63503 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..af015553 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01a9836ab3b37023d423042c45cb639a39423154e1199ba122fb1ee0fadf75f +size 13336 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..4c8888aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06bf47174bfb8c39dde3b91313da3fb193312fb4135d7aecea408ebcef47dd5 +size 78027 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..ecd2fed5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63b6a10e33f2545135692e775a84b8d4a5c643b7ea0fd51119e72e8515b909f3 +size 35640 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..c2580566 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b8cafdd179e132f5e7daec00af10aef89c303bf42ed4fc39cbcb166f03916a8 +size 73517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..adbe5210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:498906a06b5d0af6e1226440fd9b3807a957a3ba0d635ddbaaca49f3e138a765 +size 12269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..6ac713c8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b290fdcdde7882bdea6bc7714a0bdde95b2f919fd18e233f966470de9b2b178 +size 65316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..17a05d7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d711dfb562f9a0bd51309873e7b3b5a7e323ef75745f6193e546bc1ff8e9ae29 +size 21402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..6f848df5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12da7311dac6d9dc73e29b17ccce99ed2e4dbfa3fa511ef1d4d3eebcd46eaa5f +size 59723 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..c05bc10e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8428805621a049826198998399483eae917eab452e1114b7d5f6462af05e1ec +size 13420 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..4e7ff82c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e853e81ef5097ae3da7df69b12c81ce8a22a4e9889bfcbe11bb4a95310de208 +size 73642 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..3a65c336 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f0f962581fec5299101acf9c7d84c57d991ef1820f21102ee0885d6ee60b360 +size 33906 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..90860588 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessageListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad82d30ab3d1eb6ca55f971f2e8c71ca5ecc5aaab1c9fedfdfcf3abc7c4dfacc +size 69710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png new file mode 100644 index 00000000..3386a070 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53a7b22ca8c9222f59379ae0a6c1bcf7bc964fbd24f7b5183efd1f470f766812 +size 10425 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png new file mode 100644 index 00000000..93915e66 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3336dc9f8de3a344bda7cfdfac8ebea55490cec4657ecfd833ece3bc6c983380 +size 59318 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png new file mode 100644 index 00000000..943ef852 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f80235e03586d439773be075dfcabee8728b5796a3ef361f81e454bc197e6607 +size 14212 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png new file mode 100644 index 00000000..7aaea45d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2e6bbd6c5814b62088e6590d597d2100cbc9e912b908e1faa1a33e9c7f146a0 +size 49437 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..fed586b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb1b8518d31f6db5a195794b66e462d6175b6c9271ede93088331c3b1fd662e +size 11516 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..edc34f1b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea848a6fb3ace426af9f010515bdc387c363fbad2a3eb65933c068660ad00772 +size 61995 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..ac930356 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63863920872938fac3e61f51b1ccb0b887fbdb22910c6c436cd0903b01849688 +size 19490 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..284ec533 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37647528f42591f4a6b7a8b00f20553b971b9d9ad7cb19f21a5b6153e9dce3ec +size 52710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..363b6ca0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7309c4052b0728c4aba75014bc5ad70da032523be3496ad30583fff37a2c5e +size 12528 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..b680b79e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe33e18c98d881cc817e224b69d15b8c4eb47e721ca3c425e2d173add37fffca +size 69711 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..ac6e908e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc5c97d91c9ea3eab48383678bf7b38d3031df19cc0e0921ee556a158cbfcf16 +size 30933 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..c3601460 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad551df10ca7c09055d523408bd0492dd4c2f50025f14167ee73e1a2d6f4b45d +size 74627 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png new file mode 100644 index 00000000..9092081a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6897d2fb6f68aac32f41c0b138e12d3a1582a4c7da0404a84d3fc3ebd63f02f +size 18464 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png new file mode 100644 index 00000000..0c735ae7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:717008b06ac253752b1c538ca0f8fa17c03ae4c1bd0341bb16e09d1be5b87c97 +size 55718 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png new file mode 100644 index 00000000..8684b990 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9b5118888a011754753538288301d9d3d8769c8d82afe9b2c0f9dfc3e249e7f +size 13806 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png new file mode 100644 index 00000000..132fb60c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0841213a12cde8f622ea1b260698ddbbf9eda18fd8024c529310b8a86b6b469f +size 48081 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..a0bc431a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f38891a7722495220186835faa45bd9a052a0af1e1ff91a114b14d9499e6aa4 +size 19539 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..2986fea4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33dce221774de26e4c3790940f39c9fd52d45eca02567fa102f0a158ddde2eeb +size 58303 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..e330b8be --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f515d3d233500d44ccb5fe07298ee0635746cadd6d70c69409f4d77ed517a68 +size 18700 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..4731bc7a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5393107dc6f0634a45ec62297faa0141bc0dcefae463ad0bdb9817ff0b73fd31 +size 50409 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..d1d11bda --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286c1513b7bea8583dbe1cef10097afa68e5ed6bd49c44b3f667c318afcf56aa +size 20528 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..e8301b91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df5bac23e2e9d1e593cf05e29c16067f078649e5aab1c7bcc0a09108505a3ed6 +size 65696 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..17d89210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b3a67ad7e48c8354a1cd979c77a637b5e6ae20c6e08bfc185acd00a067ff234 +size 29730 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..f4de0b13 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a8108ad444b78437d49ec5587e2f5044947dddd99572aee73fe47055f39f09 +size 70985 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png new file mode 100644 index 00000000..1d252ea1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3431b7a23eb3c6ad2279242af60a6f1c3a2c34065af0030c989643abd08d86e4 +size 12109 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png new file mode 100644 index 00000000..53e0bb09 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffacd3f957711d2d4af15df76ae60fe59be7cc659351d07694be84873dab6fe4 +size 69428 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png new file mode 100644 index 00000000..76b3149b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:213e84b7fe20bda67c12f5a5f01042aa650a844c06210942f3919c5055840f4d +size 22152 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png new file mode 100644 index 00000000..b71a7882 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3425b7362bccc4bb313578281ada5d16925f2dfe77402fce7fde8ded80c5852c +size 60483 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..af015553 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01a9836ab3b37023d423042c45cb639a39423154e1199ba122fb1ee0fadf75f +size 13336 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..4c8888aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06bf47174bfb8c39dde3b91313da3fb193312fb4135d7aecea408ebcef47dd5 +size 78027 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..ecd2fed5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63b6a10e33f2545135692e775a84b8d4a5c643b7ea0fd51119e72e8515b909f3 +size 35640 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..e0ca37d4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0656fcf70c3f8f5762016ac8b29b804f590769c8689e8eb130f5e8ed70a9342b +size 84565 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png new file mode 100644 index 00000000..adbe5210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:498906a06b5d0af6e1226440fd9b3807a957a3ba0d635ddbaaca49f3e138a765 +size 12269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png new file mode 100644 index 00000000..6ac713c8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b290fdcdde7882bdea6bc7714a0bdde95b2f919fd18e233f966470de9b2b178 +size 65316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png new file mode 100644 index 00000000..17a05d7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d711dfb562f9a0bd51309873e7b3b5a7e323ef75745f6193e546bc1ff8e9ae29 +size 21402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png new file mode 100644 index 00000000..4b32f476 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:255dd4449797943f7dfa78128b519804f76ef59f4146b3ba6dbbbfdd4a7a0d26 +size 57252 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png new file mode 100644 index 00000000..c05bc10e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8428805621a049826198998399483eae917eab452e1114b7d5f6462af05e1ec +size 13420 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png new file mode 100644 index 00000000..4e7ff82c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e853e81ef5097ae3da7df69b12c81ce8a22a4e9889bfcbe11bb4a95310de208 +size 73642 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png new file mode 100644 index 00000000..3a65c336 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f0f962581fec5299101acf9c7d84c57d991ef1820f21102ee0885d6ee60b360 +size 33906 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png new file mode 100644 index 00000000..8cfc316c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.messages.ui.screens_MessagesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:529d6cea564b3d2941b949018705c04def32d1decd6ae890e1d5c120f838b5a7 +size 80227 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..d67473dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f66d14f88d7e222aedc522ae1cb2969118dd38ebfa489d1dd416bf3676f63fc7 +size 100590 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..b91ba7dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57a1d17f853d48aa816b9f1758a16b1aa8015c0a6696ae7a5524c8606d2d9df1 +size 103683 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..959e66b8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f941d36e8adfa133cc83549a9ab8c037ade98aecb840fb49682c318bb5833419 +size 76723 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..3a7344dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d97ccd55a586b373d56c98caeba9610338d475cefa4028645a44f609c806871 +size 94088 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..93fc2358 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e09868d7dd25b8f427bd0fa854d5199f3521dbe02819327ba5ad1fce8edd7517 +size 96940 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d4d04544 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:831bc181bf560961ae492b3237c14732153eae0d70aca726df12c96332b2bf93 +size 72199 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..935a59b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf19c48f5698cda3ff656046f10f596bb4d96b5d2dd15c90d1fa7b672baff58 +size 103685 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..7b34b848 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b32d49b1185b77117d20aa4119a04b676c81809b1576db8ca62d1292e39232c +size 82342 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..0cc052ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b24bbac6ca3b219bea0540c10956bb44422acb7bd5b7646e81322863e98f39 +size 96677 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..b216bd88 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitInformationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d0740cde250f6128d968e29bb9e5e95bf841ea4bc19d09501dcadb8fd0420b9 +size 76674 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..8e929a9d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12dc046d9af0894ace5aa416181cb036b091070b1d9b745c3bf598ede3c92ba6 +size 137088 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..30216ba0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:796d6b4912bf8b4e3907e345e1e4d97ca2a0233910966d9d4156e15eaf9033d3 +size 139836 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..a071ea88 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f644ab269b3a3f5f6399aec6de6fc958ceb47afa9f92aa2c2e3655177e466e7f +size 150993 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..2d0fe9c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d726f9c5493abe9b1ab3e2a7f3be695622b03e11bd9e5a88e58635549a8df59 +size 128253 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..1d4b7e52 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a276fccb5c7c1c65bc5df448d97f2b4730afe302f877e2551a4201e9b17fd210 +size 131104 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..9f1af292 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a73ecb1d956959da1863e84e13a899ba2fc5b1fa262d7de5f19d617545b99d +size 141554 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..6e0c423f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63586f7e03fccb4707e4e49d3a8f1ca7d032ed41865493cc57d66e4dc09a1a00 +size 145694 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e43914e8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba3c530dee40aac23c82b1f8672aa1d806b2796f032dc854ae098229d5fd4d5f +size 158059 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3d4b9c28 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f87466c721827bc77834836cf68981c269cb258abc591bc24df3c6871d3165d7 +size 136473 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c40a017a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.mlkit.ui_MlKitScreenKtTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed1eb0025c2d727a7296803649ca9025b90334574656142474f7110f7847fe4a +size 149211 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..ef02d021 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c52064db067897ab9b7e0a223d97e6a67887eeaf8b79244bb2d24e470887ecf +size 30496 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..81f34ad4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c83b3df0153ca8fcb7164b46c66fb5e86db360eaf70a7f589af2441c25e7779f +size 34749 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..489590f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4181762cd6cca77c4f28fd6315c047865904bc11f60476ef3aae63551b41b530 +size 62367 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..2cc18639 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16b945f6db7f205eeb9cc6ca2ed3eba7314ce9f4f28c0792d3fe2069b8c7532a +size 30087 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..842af42b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64a6a7b74ce47ce30fb166f587854655947e074e0c1ec0b3a22540df86f8f2a1 +size 33603 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0e3c423a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8143ac5352a3278e52803536c6ec413ec4907362576707148ee1a467e33f1c11 +size 61328 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..adabbfd8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65e48dd988e23fa8a95b148910acc3704f138e60a8ed754edeceb8ead8ddf32b +size 40257 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c3d6fa98 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2f09bc771b85b16efde18878530228d8b85fad1d6b646a57a8f793d81f9c5de +size 74459 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..9a008297 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec4a532396fabf04415a7295447328817847386010c8760049b82f2fbdf57a3d +size 39267 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..25915706 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_BiometryScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:971a4b2f81952741210db4a4dca9ee53ce747bd752ef0b58e8f22b7cac1af056 +size 73292 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png new file mode 100644 index 00000000..80e0644b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbf062cc734255dfafa36290299759ccb55ad9eb9cb4489407645259a94341d4 +size 36951 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png new file mode 100644 index 00000000..d88a1e65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aed39ab80a6d0790f07581693718dd99dc5c4882e4eb8d06644befe636a7de7b +size 37106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png new file mode 100644 index 00000000..9ee0e5ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0170964fba9a8b0fd4380ca4f6c46ec34c9cecc24284590e3046189af5f856c1 +size 47169 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png new file mode 100644 index 00000000..9c783872 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eefb6ddf99e90eed9113343a8ce81e3dbf358fbfdfbba237fba6477bf98c7dae +size 47398 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..0ef85309 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a774ef5107d844eb21b3fc72209987f78e0510ee52f7709aeb5833063b6036d +size 60308 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..0ef85309 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a774ef5107d844eb21b3fc72209987f78e0510ee52f7709aeb5833063b6036d +size 60308 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png new file mode 100644 index 00000000..2702b698 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e47ccd03bfa38c4b6938cec69008e38382f7aa5f2b50e614b1c54d6d880d8e1 +size 36033 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png new file mode 100644 index 00000000..d4dcc82d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d8195796d83b50c5d494ff5551c113ee6c616fefff74d434e685ca92715f879 +size 36353 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png new file mode 100644 index 00000000..51781ff3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b36dcbc74432dc773df0883aa8004db1f770e6ab6c76efeaf24404224a76682 +size 45798 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png new file mode 100644 index 00000000..af3fe25e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98ade9ff2a52495a83877b9652482439a3324e76d14b1afb8f433a7d5a420a15 +size 46150 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..92c4104e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cbc5292832b98279e126bdb5bdfae5bfd166397ce3af5c5dc1ceb4bbd235b88 +size 58424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..92c4104e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cbc5292832b98279e126bdb5bdfae5bfd166397ce3af5c5dc1ceb4bbd235b88 +size 58424 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png new file mode 100644 index 00000000..30fc08b0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8874d9fcb8ae2a266b9d70d0079a34fc744b5d159f621c8ca733f53df538f63a +size 53979 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png new file mode 100644 index 00000000..5a17d82f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a3878278438b8c31a8e5fa0c92ca46f55d9cf320ffdf6ff9c7ac5152bad6a02 +size 54130 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..366397e8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9cb8179d2ce7eecc77a8f33c382a01fcc010af74cecc8bf3e651afa2f4b731b +size 62806 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..366397e8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9cb8179d2ce7eecc77a8f33c382a01fcc010af74cecc8bf3e651afa2f4b731b +size 62806 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png new file mode 100644 index 00000000..cc614ba4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeca5e1b8632d36650ed0e023a485ffe0e0b98fab1224b3cbe4e105eb6530f85 +size 52445 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png new file mode 100644 index 00000000..7f3669a9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:093cb08691343ea6a9165898aaa750cce177b5626c01029f6935dc972fd649a1 +size 52864 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..8ac6867c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ea8d9f8fc3b7bb1a56b454c0d4cc80ff6bb76e69eb7238728a8db979b7f9ece +size 59932 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..8ac6867c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingAnalyticScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ea8d9f8fc3b7bb1a56b454c0d4cc80ff6bb76e69eb7238728a8db979b7f9ece +size 59932 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png new file mode 100644 index 00000000..4bbd40c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f08954c15fcf725cd5b911c4189e759158c410064949ec9b65ebb78fb127f47f +size 52243 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png new file mode 100644 index 00000000..08940912 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6064a80b309a1395cfb89889d719e0ffb1d3e612ae2d4cd33ee640aab324441d +size 52869 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png new file mode 100644 index 00000000..78971d7a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddfd996712d6975037b1292db771ad7063857b260ed807cf95c506ad43413a71 +size 60664 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png new file mode 100644 index 00000000..52bbf77d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c1f3fbc1fa69ddf4078c069503dcd1b3638cdd2a2e654e923f4c95e585a77 +size 61394 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..e6f5875c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:852ea5c488c4e420659410b14270b813d70429d78b51108051ba998660ec3d0f +size 71984 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..264ed493 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2562c45f7d981141f0c20900748de96b761f9528be41cbfa424efe140c40b9cb +size 72970 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png new file mode 100644 index 00000000..3ab49203 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4f4dfaf4f915abe75b163d8a5a4aace521cdbe35e2d5ec31ea36cc2f2f76a7 +size 51977 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png new file mode 100644 index 00000000..3c53bc39 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbaea2f4b236c24487aa7ed1351a777fbb4fd709c8bdea179e112f8c1687e982 +size 52579 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png new file mode 100644 index 00000000..b9c59812 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3ffe39ebf4c746b837e7bf6d7332dff560efb249955b8e7f8265d3b999b86e0 +size 60495 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png new file mode 100644 index 00000000..e9b7df64 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2144a92d8a519103947a44d1ab4a0c7c630b02abda096ba773852303ed04edd +size 61377 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..fd87038d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a55045c3d3d0659da9d05bf33076a52f0407f92efff793161137ccf440e01c +size 72158 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..593c0463 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a89226af342def4e55cb077de51c5017ecd524217f24b322ed1ab8ba6e309fa8 +size 73251 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png new file mode 100644 index 00000000..0dac6f9e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b8c2aeaff7dd4a962d976c03666dadc037955814e20ad1b63f6e5b35ba4dfc +size 66176 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png new file mode 100644 index 00000000..fd31da6c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d855ee77b91d7a76a28814340d951cba0af573fb3f1d3deae8778d6afcac746 +size 67054 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..d3844289 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:522920a618d32814adb432170193ff5adbfaf232808c0908709b0962443231a1 +size 70223 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..854c53a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41500ff49da8712851c0563ab306eb8a6b6b2f2cabdc1741fb308e2c6d78c87b +size 71348 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png new file mode 100644 index 00000000..97b80f8e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:398e52f96f02e7c9c734db97c1db21eb6342af3babd7736462696c6d65aa5c9f +size 65671 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png new file mode 100644 index 00000000..7c939dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea66cb4b157cb2ea9887cb49b9c3f7eeccb6be74034b8000a1c1da67fd367110 +size 66715 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png new file mode 100644 index 00000000..c336611e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d07d28b4e3211b7b827f9cd863c1e6330beb59fda35c5417860fb1bff0019b +size 69893 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png new file mode 100644 index 00000000..aa42bf0e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingDataProtectionAndTermsOfUseOverviewScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6ac77aaf3e9f2c5c0257c6f21e4e66d8a032e5f9261893ff64e207ab1c43047 +size 70919 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png new file mode 100644 index 00000000..a40f4f72 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bdcf40fb6e588c525fdc983096b0a0cd9165321a04a59b823bb5a3c450877e8 +size 82754 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png new file mode 100644 index 00000000..4cce3743 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263a5ea35f049f9e11f863157f4e2add01e1d280aad3bfd152c5325a4e2da277 +size 80525 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png new file mode 100644 index 00000000..f2f37ed1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f45715f8754db3cb8aa7d17775fdcd7bc7ca4a4d265250df54d3a352acfd10a0 +size 81416 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png new file mode 100644 index 00000000..9ecd21cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111f611674155094c5301f5bfd8fdcd02fd62a948debd9dc636063980382e2d7 +size 80082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png new file mode 100644 index 00000000..88e0c89b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56fe7a2cc053533db8b041b499ae860c911df7602480b05c5e2af4b31ead93e9 +size 84234 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png new file mode 100644 index 00000000..d6ad7cd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a21858d55eee7ed68122573fdfab8ef67660a50bede4e2849abb3c063ba88a +size 81421 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png new file mode 100644 index 00000000..34f6533a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b25a1d362ddf0e8827d2e972ac61a608ab816431895fde4e31c6077705c2ea1 +size 81713 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png new file mode 100644 index 00000000..a61372e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7887b94f60586c82d70a74a4ec0ba1ff868379a5083cb10560cb8efb9f96b5ad +size 80954 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..25428418 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b4f1167dc3169461b9103a71b087d1303d92b9a44b55e15b018618f2f947d45 +size 93251 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..e1a54497 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b67556588f04cdad618f9112478495b1066f0829f7c8008eb440873e2cb8b9a +size 96212 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..7e7331d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c24cfdd122f3bad28e92af9761ba185c8ac788df81a532cdf8286e7cd2837ff2 +size 96574 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..4d0cf1f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a75409e6c5d6d0dbf6ead7366e0651352140ce39b5e60175d2edf3c3bcebe235 +size 92623 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png new file mode 100644 index 00000000..0ef471f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620164dcd32567b2753b184e0e176fca53045c822046cfb011653da038514e04 +size 81245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png new file mode 100644 index 00000000..0486cf05 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51b1bac52f4a1e4c96a406b6a249ee108948fbf610452768c648c2b17cfef1f4 +size 79067 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png new file mode 100644 index 00000000..324a5268 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9ed5eddfcfcc708b425d7ae1be4b33201a2d526b04d3a6a9a36eece99f23f19 +size 79843 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png new file mode 100644 index 00000000..65abae90 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eef816ac112b80cd9a29392d4cf4fa45241b06bb7bbd97c01f46bfa4399295dc +size 78315 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png new file mode 100644 index 00000000..a840edae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd10c4112719c74a3b9dc876cb22d1cc575c983bbd4cb27e8f34959f95eaab1f +size 82633 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png new file mode 100644 index 00000000..5083620f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328e882cbb4f05323c942d1b03843de924ae2387f6a7efe5a717ec41fcb44f47 +size 80106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png new file mode 100644 index 00000000..321323d5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a213f69b68ae66cf3e5109daa955c4b6a3c90e75cff7f41add26faade1582bb +size 80337 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png new file mode 100644 index 00000000..eb09a76c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd4b6fcce2bbc59c45384e36184c8c5096285f744267033c9b5eef3fd1a321d6 +size 79062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..dea7f7fe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b97d6421e3402ae8763005e60767de926a7606fc0e633e1561f2fc457480bff +size 90782 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..2d7bdd0e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a430a71accc4cc094cb52707a30733684928b6568f5864757d3371ad5350a66d +size 93865 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..12130e93 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4164b9a2a6fde0ec7a7bfaf8e2399a1aa1396b107b59a6f4ffa8a828daebc6fb +size 94082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..2453eb77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abe0f00976f6fc5c7f1d4097834f3b55227b65b5b7f4d631f0f08b0f90429b27 +size 90259 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png new file mode 100644 index 00000000..f1821e3d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63d34646647cd31130a84f47e57588b2be9472bf822f1292063f6f253ec0b4c1 +size 91839 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png new file mode 100644 index 00000000..be52be26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90092ba483b7f9a0b665e328ee1dfa7b7597e923942ca46030b3e4343e6b03f9 +size 89139 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png new file mode 100644 index 00000000..add545ab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f10c4ac9e23bc01c30015228d248ffe9a72a9170eaeef4884b7f087d07bbee8a +size 89525 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png new file mode 100644 index 00000000..2a06be81 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a0624f9f9add870ccf2d977ccdecc36816a0e4c9fc2a5225d7c7935a20cfe42 +size 88315 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..a209bff8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f03e4fcf3e3258435309f0b72b941a554b41cc14d2805dabc61d7e2c2e690b58 +size 95429 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..db0d39d2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa6f7174556ecd1e1c6875f99b94478f5f68feeef59f199ec0a41c7fe0572db0 +size 96956 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..84be0d82 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32ba01dac15522cb5791263bbb1f9594f323b8f47d328376b68f6ffa14a72eae +size 97953 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..4621d572 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efbbb37cb198b4d782c7ed985354a83c5fd93242c6d508d66d38c6e7d5fa9d9f +size 94706 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png new file mode 100644 index 00000000..a072dcc0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3974aafdcf480b4b47bbc6990449f802e28cafc3412abc8919b1c7e21deb7337 +size 90022 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png new file mode 100644 index 00000000..394b3859 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9cf8a39266c8e821d581a807d53f512b506f9e25f1851066ef002a1fd5ec241 +size 87234 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png new file mode 100644 index 00000000..1f115961 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f148d998fc2e9ac26d66b7acc076ab23511354f828e76f28ef610d5d74de3d62 +size 87568 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png new file mode 100644 index 00000000..bab71871 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63a0e2974868552161d717960c26f44d890931bdb4183ed100e2d9758fe9559d +size 85923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..eaab8e64 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b31dfc5cf71166e7ee8ee317183e98d714998d12668a64ce672cbc9a8e7e8c8d +size 93359 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..7d834400 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b717015f64b0f6567436e4ffefee6c7ee9d67dfb55a1eb9dd1ed0fe41813a1 +size 94670 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..59b0e41e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f4626402a2b0af0540d2d40c77b8f051d7c9c8db06915ce63a3593bc162260c +size 95482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..dac903e2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingPasswordAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4444a2c1a5b6dff59f41d928f61cd5a67143f33e3bc42f2550ae495d1446ca4 +size 92615 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..2da29263 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17a327e2bf0cd7679b0b2a7d28371509b3f8c810b30e5cb7792bea732128b694 +size 359209 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..ff9d3c24 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a93ea9bf953778b64631395f1aa88af8fec0c2da451f34debc8ff555458f1f45 +size 359573 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..2555e5d9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eab3fc49e0dacbd7b2f64750324ae2a55789335b32f0d72661070a3b6010dca3 +size 362106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..30f78bf5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:929e06a2c5452cce52dcba76d607d37f179bd957f555499a14c5f20fb1660510 +size 359299 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..cf4683c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f2328fc36e0d1f3dbe28df899094e752dfa6da4898edff22a0da5ba7f28192a +size 359650 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..37803d75 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3d1081816a42750e156afafa5ba33ec85d45bf2bdbb389538bc49dfce6ffb2c +size 361981 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..13829543 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e6c6fe63a3a7bbddd85bd7648406ac99b91655558fd32aee5fc8bf1e99fd4d5 +size 370391 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..86c67609 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d8474e3752b96c3b62379da5670ed25ff66bb9125fd6c3ed9bc3545c65f07a6 +size 369997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..7d9d6334 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b8249195af91a7429789030d2b4d4132c0b3a369055676d38a84fbf13425e9 +size 370023 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d46d5ba4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.onboarding.ui_OnboardingWelcomeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36c31e0f2a0e39f90babbf9de17e0a30708b5f1107b45248118f33eea8c782d2 +size 369468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..276aadbe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:180822c003f78b2ec82fed8f4b12bb8ac06c4080630cd0f8c480a2c4a60e6dfa +size 9544 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..276aadbe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:180822c003f78b2ec82fed8f4b12bb8ac06c4080630cd0f8c480a2c4a60e6dfa +size 9544 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..276aadbe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:180822c003f78b2ec82fed8f4b12bb8ac06c4080630cd0f8c480a2c4a60e6dfa +size 9544 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..ee2a815f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7581f530ef274a22c4e1dfb68c91946b937b2fa47c3841090f529a3fac24331e +size 76241 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..92f693cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56bf65d2219f218fba8d2dd096ddb70d4fa7623d50395f9c639beaf20eababa +size 9292 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..92f693cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56bf65d2219f218fba8d2dd096ddb70d4fa7623d50395f9c639beaf20eababa +size 9292 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..92f693cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56bf65d2219f218fba8d2dd096ddb70d4fa7623d50395f9c639beaf20eababa +size 9292 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..46c15119 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87b3ba3a797206dbe9893df292aace12580ea8c2c9b65bb260425d7de4429a6a +size 77468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..7cb0e113 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b352a1db3a9003d12b9bce53593d40affe0a6c20593654076179c8ca2beea6 +size 12161 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..7cb0e113 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b352a1db3a9003d12b9bce53593d40affe0a6c20593654076179c8ca2beea6 +size 12161 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..7cb0e113 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14b352a1db3a9003d12b9bce53593d40affe0a6c20593654076179c8ca2beea6 +size 12161 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..91cd27c0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad026476339d5c7111be517f26f453d19b636f2f59e7c0ee0d58a7fa0703427c +size 78620 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..a068ef68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4df29a1af45e54cfd49f7f3d215fbda982e147058de9273bf006930e98e5c90c +size 9423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..a068ef68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4df29a1af45e54cfd49f7f3d215fbda982e147058de9273bf006930e98e5c90c +size 9423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..a068ef68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4df29a1af45e54cfd49f7f3d215fbda982e147058de9273bf006930e98e5c90c +size 9423 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..486b6203 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5deb9986f81a3683322f2ca109a8400656daac54b793f330d779bc705086daf +size 71136 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..b726be97 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40f87f8197c6ae172bc2669cab59c261d43210862f8d9c755f24bb11b4900d0b +size 9129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..b726be97 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40f87f8197c6ae172bc2669cab59c261d43210862f8d9c755f24bb11b4900d0b +size 9129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..b726be97 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40f87f8197c6ae172bc2669cab59c261d43210862f8d9c755f24bb11b4900d0b +size 9129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..59c38028 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f67256663054ad4032c2c149d644a78b42d0ae7fa032ae5c89e04b0bf3763ca0 +size 72094 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..815c3066 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6f97d66e367a849ede5d7cc8519f610b70cb7a0b136fed7af8cd29688a83bc +size 12022 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..815c3066 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6f97d66e367a849ede5d7cc8519f610b70cb7a0b136fed7af8cd29688a83bc +size 12022 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..815c3066 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6f97d66e367a849ede5d7cc8519f610b70cb7a0b136fed7af8cd29688a83bc +size 12022 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..6f8f3a23 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc67d7f31bd8ff2589e4279210626736f6706c0e0ce4376447f95999b1dd972a +size 73889 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..6db0a513 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124bd7ab3c143ddee032358052bbec7746a03612ab11e8b5d7e05c0a1757c802 +size 9923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..6db0a513 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124bd7ab3c143ddee032358052bbec7746a03612ab11e8b5d7e05c0a1757c802 +size 9923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..6db0a513 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:124bd7ab3c143ddee032358052bbec7746a03612ab11e8b5d7e05c0a1757c802 +size 9923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..863fc94b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e019da1bb0ad55235a119455a4ba22d464596047b49b018e7a4e44be4c97fccf +size 87740 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..48aa03fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9d82dc28e7ac3b3c3ac9415feec3d947a5b8349ba9998d8cc4702b2ba76bb40 +size 13318 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..48aa03fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9d82dc28e7ac3b3c3ac9415feec3d947a5b8349ba9998d8cc4702b2ba76bb40 +size 13318 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..48aa03fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9d82dc28e7ac3b3c3ac9415feec3d947a5b8349ba9998d8cc4702b2ba76bb40 +size 13318 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..33d6c464 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7ed734bec8dd11aa1447eec4ba288592e466ee3de42ddb08a86e501a579abc5 +size 87043 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..ad4e68dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4d28280421ddba5a9ed522b20abb00200b37bfa339754ef7a00c99f931b56a +size 10137 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..ad4e68dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4d28280421ddba5a9ed522b20abb00200b37bfa339754ef7a00c99f931b56a +size 10137 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..ad4e68dc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4d28280421ddba5a9ed522b20abb00200b37bfa339754ef7a00c99f931b56a +size 10137 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..02fb8720 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:182e03d902154306dfdd82e86dbb9adabb5798e52e23876e32acfaea7c374ef2 +size 81685 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..9d8d5514 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ed43caf695c1e5f462ec98f5d1ef257b83fa5ee8f67ede4925d8f923e5433f +size 13383 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..9d8d5514 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ed43caf695c1e5f462ec98f5d1ef257b83fa5ee8f67ede4925d8f923e5433f +size 13383 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..9d8d5514 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ed43caf695c1e5f462ec98f5d1ef257b83fa5ee8f67ede4925d8f923e5433f +size 13383 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..7d8591a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.order.messge.ui_MessageDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94b24ebcc3114943bf98c25c8c988e505da752c2d1321ee19bd6ba468c74b6bb +size 81762 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..7b6f8d45 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0e726d26d87bc040964d0ebdda1d1ad90fa19178be94be94bfb1441757c5ac9 +size 35343 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..37008d40 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd546cbd2d97ab7085bb946800d2c2b78690561665a907689543a599c4d7eff2 +size 15949 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..dd31c495 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12b67c40136fcf0759dff07fdc3fd5590e6a86985c8efe7102af979002b99f4a +size 35346 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..1f063a89 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4a75dac770b98735c38d9569b0c1ac6ba81f48ba8d1a9ba1ceec510b503aa3e +size 15329 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..acf233b6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b74da829d6caa96148bdbaba1780b6a7e2412d6238eaf94a944daafcb3d5690 +size 55141 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..bf60e355 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ec9e505be5013c56a9af23fc19eee408d871170a541e94e879c45ce4cc858ed +size 21618 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..4a448220 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb958e9b47dcc51cafbf1deed627285f9b1e1b119c6275f59f3efab519faf1bd +size 34093 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..161874eb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d2b77238ceab9a6aa089a3f12dcdceb40b8edb31f6ed0a3117f120bf3154fe1 +size 15614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..c460a378 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:996af8c76b6d12f1b42f0816be482ef1314eea1b3129ffffcae59c995b199bab +size 34219 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..4432b468 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:116ed677ec2934676a5343b3cee54ff87a0fc5707a27b4127bad420dadc7baca +size 15029 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..40b50785 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9440d66860dac13cd57835cb97690143c4a56745b99774a7da0565b4aca35e6 +size 54293 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..aaa65bf9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ea434e678b55d4ce4d31e6055128c649a09faf86c8f966ce8dfa6355f4268d8 +size 21221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..2caea7ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c29f251680b0e419284747600a94b343dc351c433e6348d680ab04d1daf6ba0 +size 40384 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..6c78842b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d097572192eb7573f533883f2f9fff878cce229a4eb1356f77cd9f33dc880b4 +size 17396 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..73d9baad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c884081acde1b840d5e8d975dfaae02f2ff7b90370f498c1e73ef2044dc9c4f3 +size 64157 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..b1c97a7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6128a9e85215a966e57974c5898d7ab82c7c11e7cea93a7acc135c9aa64c5011 +size 23957 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..4acdcd9f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61dae59c0eb14b65cf381a2a0c0b7fec5dc60fa3ca870f647759ff5f82303151 +size 38999 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..df901d53 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fb2ec250886acf82ac2aab4757ab8620b4fdc718ddd07317504ea1256059dc9 +size 16939 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..91884205 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3836291d5b3d8d3cc7b6cea776dc6b286b6c93ba9812b626079cf28d29a8ecf +size 62934 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..2c300695 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectInsuranceCompanyScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7aadc78bb2ef3688edafd581099ca632025908cf7157b1799e58bc4ffc00133b +size 23597 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..37796f92 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d756e6a5ecdddffc1038a4afa87f3330384bcbb37dbf64da6c50aac7922cfcb3 +size 28056 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..cfe00f21 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5e821ab8a48a573962a0dbb00c71997d8447210eafaece9de35310651393cc8 +size 28043 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..ed742f52 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:345d56eab0caf4481eb678cafe6fecff66528a02d4c4265ca4ff1bee0616aee8 +size 43127 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..d52222a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b9006e315bbb47004ea9eacb2885de63d7aa660b53fcdae4791dff7cc9e4f4e +size 27144 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..4bd1021f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c85dc47268fb2425fef586ea0e55fa0e8410c4cb2bec722ebc276f29820a8e1 +size 27051 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d6f07f17 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e334a886d5d5be930e4a4b6279934046d39154ed71e37710941728a6fb0d25dd +size 40760 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..d3edb71b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70400e3a8cb4052d5409dd9465f0973610528a341ddc8297bde3588863ccf401 +size 32387 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c320a0e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23ef82a8ea4b6e36274b2c08319aefe8d9a2b2b9f03939a039d8bbec55996d95 +size 49864 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..fca0233b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87cdb8c07ed433906e99e9856c13671ddced72e8790e6f9d8827484b1325801a +size 30354 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..79cb1374 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectMethodScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263a66594afe4502e2fb70b183e14cc8a9998f7482312e19bdbab23a5a32ec52 +size 47114 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..8f68c356 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07f8af4fc7cdea2f32ead65bae82600638fae3831b515c80897dc59dd3e63e97 +size 130093 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..d375ef16 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbd38e0180264be01180d0a7cb07fb493db6d4d096d18cbbd94ad55c194c6387 +size 131495 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..b901495b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:805a853f77ab4d13edb9a1c1f7a1111fc733aa6fdfec361bfef6d600d66a2967 +size 144230 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..8bfe52fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08328141b795d56a6ab68cecc97836033fbeea4a577ac9e4936ec6ecb8ac7305 +size 121708 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3817c34b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e72a64c8984b28ad97ea44f14d4214a8e5a16f6d04b030a063e055c48537d4cd +size 123415 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..2759ba57 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47a3e1822fe75b91565f642f90edfa1e32e1c0a53c4b2397bdd6a69a1744d1c2 +size 135641 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..ec487f44 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd617e7bc7360faf10e853ca93b0c1660cb4174c66b0cd58c66c2e1ec5f2d0e2 +size 140503 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e886bdb4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcb806f3fdcbeb8e1f24783786d7c8d79e927da38a2279e6b44967ceba2a7eeb +size 150095 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..621cbd87 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f999fdb8c375a6409b36e7d4665786dcaf9d86d008124895dff76aab652a50f +size 132953 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..337f14d0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.orderhealthcard.ui.screen_OrderHealthCardSelectOptionScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a4f8270076005a3f373bc9bebf4a6a0056a36e6d5fd5c72f8e2de2ac1949e31 +size 142673 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png new file mode 100644 index 00000000..d248f6d8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ede1f84f956ec66d1e20bf575c389e8281ba9d5ddcc1c65946b579773bfe174 +size 119522 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png new file mode 100644 index 00000000..ed8bab4f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49a78213ebee2fa40ca018b81c1fc116044686d741b5145c69ea38b6bc7f5546 +size 120896 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..301e0518 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9982e8c071dd15c4afd0b70e5933518a9638b215d1433086e40a41742fa81a0 +size 66566 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..401c939e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:422feb54fd2ec7ce9e67f82a5f714a4466d5d21bb3cb632a6c7228668742c445 +size 66719 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..6dcbcd1e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3e96bf20e1cee44a39c1aaf2d059cd38eb39012307c216e7b4b3225cb3907ca +size 77220 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..44dd5682 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2860b24b12402f356e70ddd01585136e0349161f61746cac7e0803836a49c47c +size 62053 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3b681ab4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60a330ebc2fd07a6dde127dca9b01f223799a569a9229f044b6969d4caad0af5 +size 62140 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..9f062fb0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b805258f1dff30feb81f741451c55f119479d6d3cfabbb300c49a54913418709 +size 72829 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..6cae881f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3661334b84c1e1726e88e9ca2179144ad600a3159002d3c42cf140d5dd808ea6 +size 71094 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..ce77d7bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:995404c28a6e2e086a580d2ceead1b5d18807dcd1eb9eaaefe6a51e608f67041 +size 82521 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..bd515183 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ec6ed7e1f78ac2b299148a43c24b9c87e1e3e35c47bec4e2a553ed7a95b79d +size 66476 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e858225c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromMessageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:423c7e62c0287f18d61d14d2ed0f8ea7ae00704f40e3e7c5d2ca46e4cc2b8385 +size 78166 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png new file mode 100644 index 00000000..58bdef43 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f6706c378c0db396ce99d4031018e221bb2da5912b0fbf1165a82cb3cc7f9ba +size 139221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png new file mode 100644 index 00000000..1c2740ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd790d2624afc51a31146134773b13535a294a228a9f8eedc40457100b94dce1 +size 121757 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png new file mode 100644 index 00000000..cce83bcd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1e34aa256647cdf8779ed049c203728a81925b6609d665e77dc818695ee7747 +size 131412 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_3.png new file mode 100644 index 00000000..cce83bcd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1e34aa256647cdf8779ed049c203728a81925b6609d665e77dc818695ee7747 +size 131412 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png new file mode 100644 index 00000000..5880a8ac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d74f6ce4ae6ce508d7a97eecc9b34191efbc4b7a4738876d3c392d73e52a25ba +size 140961 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png new file mode 100644 index 00000000..afb9fcfe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a236f32a6881df44f12b7513b507c838deca1b4e06c4bb48709425d68ce44edc +size 124196 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png new file mode 100644 index 00000000..3175f7df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31f232974889e0f43df2fb46eb5ed1e4b61717cc9884530c65ef021ee951631b +size 133958 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_3.png new file mode 100644 index 00000000..3175f7df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31f232974889e0f43df2fb46eb5ed1e4b61717cc9884530c65ef021ee951631b +size 133958 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..ca016954 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3943de2f2f623001535d58e16d63178faf978331fe74feb6a9638ce5924f64d +size 84625 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..0d73e2e6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79855057db7c02bb2763bdbd33aaa4ce6e3955e1d5953c1a9d703fe4c785a292 +size 76826 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..2c47c2f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a145afed6efa0df1ef76e89ac377fb21462376e62bf9470632fe1816b8a8a7fb +size 84129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..2c47c2f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a145afed6efa0df1ef76e89ac377fb21462376e62bf9470632fe1816b8a8a7fb +size 84129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..3dc0671c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40bba25e85cfeff6ff15406d2e4a1adf55ff1dd178b72edae356eeca9d2eeccd +size 84299 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..28da95f4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55ad91c4466ad865130194385942c669f55cff033640cef6f68c5f45c7ab09cb +size 77326 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..edf10a68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39b0b4dbf0dbeb34440c21d0ebee6c61d3eba0ddd5b8b92813590300930cd350 +size 84371 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..edf10a68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39b0b4dbf0dbeb34440c21d0ebee6c61d3eba0ddd5b8b92813590300930cd350 +size 84371 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..e225b2b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:228ea08b9918017551f21139b72de96e07705c2454a21b6ab771a4e8b1dfd1f3 +size 92390 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..043921a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e94d72abb668bedd0a053725cbc24b1bbb752dc1e5f75a9d06d04ede9a10017 +size 79795 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..7d51dc7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539bc459980920d5eb98221c9485a90f60f3ce2ba2af882ff3e52095bf4b617c +size 87218 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..7d51dc7f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539bc459980920d5eb98221c9485a90f60f3ce2ba2af882ff3e52095bf4b617c +size 87218 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..967d1850 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c46b9af0a23efa96bc92ce36b1a05471c596228dc9776a8adff9feb6b7ecf46f +size 82054 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..2afaef26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efe31165932e3b1ddbcf9dc6be3aac338cf17519de977d41745921fbd0d72ddc +size 72127 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..fed14cac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc120e516a871b9ec6cd31a2b9fac9fdb45124d9e9643ed28ee50ae4f1e328c6 +size 80091 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..fed14cac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc120e516a871b9ec6cd31a2b9fac9fdb45124d9e9643ed28ee50ae4f1e328c6 +size 80091 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..0232e461 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b94dd2f8f9f59c231920b874663cadb2e5fadbf024cf4624ae087545ccc9e0e8 +size 81662 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..0e8487ac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8da3a41ef1d5b02893ecd45a2c89b29ed92e6af46b9d0ba69d486b28cfd42af9 +size 72579 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..6bcaca33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77aeb03b497c9c785a13862f9c4ddc9457ca27f107e986448892b1aa3fef1b2e +size 80346 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..6bcaca33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77aeb03b497c9c785a13862f9c4ddc9457ca27f107e986448892b1aa3fef1b2e +size 80346 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..9c9a89da --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:294f1e53ba8fde8277c491d477848087cb75f9619946891d572f91edf6db6e6e +size 90010 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..077498ce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a94b96d2c214f1a533d5129141a368405381434aeb1deb5f2a8a20962e597cb +size 76034 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..25fd9bb3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029cc76ffe0576ab9d8d932ddc3ab7b505e3ba0007be28d399375e5e4697e69e +size 83963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..25fd9bb3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029cc76ffe0576ab9d8d932ddc3ab7b505e3ba0007be28d399375e5e4697e69e +size 83963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..253108a7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e09451179cbce776a715c8af2ca7314e5e27a21b737ce04dbdb603d63d32158 +size 87981 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..bcce429c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2902e6119cc31611f3cedf6d91e5c9b825f22b07d9281bfcb9f2a56ff79cb36f +size 80732 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..4f445aca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:501e624e19937095ed4884d820d6bb4f6abc02619fadc862a0c6ca9f2ecaec68 +size 88390 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..4f445aca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:501e624e19937095ed4884d820d6bb4f6abc02619fadc862a0c6ca9f2ecaec68 +size 88390 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..0d249a70 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc30fa4e0f62844d34b0d7a5e7cdca821b09097986ad94e3575ba4177d78265c +size 96932 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..a938284d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b60d76f139010b133fc84840d8956aa8e6e2d8899fa59d564e10831d0ba122e +size 84015 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..d6b52dae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a7bcfd5bca1dea09bd6c801327eb12a6fdcb66bc6e8285f4f22c06fbeeff870 +size 91504 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..d6b52dae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a7bcfd5bca1dea09bd6c801327eb12a6fdcb66bc6e8285f4f22c06fbeeff870 +size 91504 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..f04cdb34 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81d6033954fea037bfd37435b987559f4d66fda8f1820ac2a1f02b408f0126f0 +size 85041 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..88bc951c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc7bdf4b0cbbbbd6e89ae0672e33af555b816a6cf958dcefc6d5c1a0327486bb +size 75924 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..7ba50d2f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e531fe06635d81a7e0d794d7065469645e75aa0bfaa8ef7804fb89dda0397e29 +size 83876 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..7ba50d2f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e531fe06635d81a7e0d794d7065469645e75aa0bfaa8ef7804fb89dda0397e29 +size 83876 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..8f7c9c8c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ade4049c7b5a640b54d79696126424fcd54d4f9dc1737795d09dd255ac54ef6 +size 94602 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..d3e9745b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3afd09558769664ab4a279944aa26be55e224f57f18538eb4484fa27c9328d6 +size 80221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..85d10545 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6032599b6736eacae2ef08132f375e3075da817f8fcaedf1d87d0b8f640fad1 +size 88614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..85d10545 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.components_PharmacyDetailsFromPharmacyScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6032599b6736eacae2ef08132f375e3075da817f8fcaedf1d87d0b8f640fad1 +size 88614 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png new file mode 100644 index 00000000..c4553b46 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6a23213eced4635b7572710dd822e2cc949ab5b188448596d8b687435e04e61 +size 26469 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png new file mode 100644 index 00000000..3cf43352 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16e7625255e47fd351d9d36d95380c754ff2c3a4f54169e0daa42bd5e35e2828 +size 26825 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..f074db85 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db67002a08327ab8538a84857e79f4d28e9aca92907c00363b69576e1626a19d +size 15232 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..f4b518db --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1f77f5b414f38a831730fa1b7b1c7e867d54885f396e3f277f6f17727166cf8 +size 15978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..3e5b95e8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac01d8a07724b159d22cf4a0a33003b842039da8a1eae14d29cc7fec6e011daa +size 23228 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..16e8d5ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0158ea608b9d33baf2ca58499073b6e2d45886b5d9acad1edabbb77c46571bbd +size 14393 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..bdfdd1e2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559046983ab38afcadc65b1823c40bc2af706a234c6102515ca95d5e45b5631e +size 14876 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e10a0726 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5a2b04a323886cf0c4d7ef54fba0e6777bb4ebae5932a57b20c57514a634c06 +size 22106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..8d5d014b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5604dbacc1171ca4b9f1bd42e3a03117c7c1ce7341243bc19fadda8848691662 +size 18245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..7f08685c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abee82da24b1ed53e01b4d5e7de86b03e46a8baf82f62703cefab4d826150fd4 +size 26131 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..b340e126 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b3b7d9f3ad054c64346998a9a4cbbb2a8fc0255053c462a2fe3f91356ced3d9 +size 16914 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..4eac5d48 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchFilterScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caf83a94db0a609fe26d6efffe2bfd40f477f410ce55cf592d8e2b057b062876 +size 25200 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png new file mode 100644 index 00000000..616a6dc1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a47cc52fb7e7447f506b7f594069844f3e45473245c22a9258100d0d5ed773d +size 51916 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png new file mode 100644 index 00000000..36faf84f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8984afeb52194f77a966d1dd5c3afc40caed5d2b9696247a37b04613e1dd24eb +size 39774 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png new file mode 100644 index 00000000..6b705c29 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:953b628a9e753d02694760f154fb3a1e08bc7f6873d16050b61d49a580b182b1 +size 41888 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png new file mode 100644 index 00000000..b15ae87b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c41fed44fdc2fc9e7b1c2410b9bfcd0a288aa0c4e1d0cded57710805e045fe6 +size 52768 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png new file mode 100644 index 00000000..a409468c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a452fe445a681498a6ff0cc1ae474c28a9ece859990b2f92d32c27e15a54fc05 +size 39981 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png new file mode 100644 index 00000000..92f705ef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4feb98ea329ce86cacbfd12c0a655f16bc6c8a99a249f60d0d23c771e97e80 +size 42046 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..cb2c11aa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e72145dae647716998d046fd73f57e023bbbf7740a4a76fb6f6282bb1d17c76a +size 19750 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..bb8e0ee9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb26313b321eb59549ae37ce6de3a82455c080091e8286480aa0810e6161c556 +size 14546 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..a2744723 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ce903da9f977778315bf0403a230649ed09e8a287d4aaec510147c17c99cbba +size 15249 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..560a2619 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1397bc6fda8367fdb20e08762647167990ae9edc1934a59b6f09ce8d2a978ff +size 19752 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..9c7ba7ee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1460afbd9e4b6f0f9c5ca9338036b88c36fa4df5952d569b9ffca80875416bc7 +size 14682 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..cc5e2803 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0174d8718d0e93f319a58c35ff3f650cba0630b02f1088ce8bd015bb34d47171 +size 15260 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..5e0d94fb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa4e155100daa7f41ef0d0e62bbe77054c408f4cf747f4b2487191237bc11d41 +size 20707 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..7f45f4df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a6e39cc2e68b5c116018987b8bdea14f998552ec63f0c350a76850ba886b94e +size 16033 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..f2dbbff2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa3552a9a26d538aa0c1f15ae79c22e5c40e7e044f56130c6012ce510c6adc64 +size 16629 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..c38b1142 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57b47b539dc835f97900252729affbbdca0801332565e75d333d094d8205df08 +size 31246 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..3f0f194e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b723193d293240555d4b5607780ece378c16a5ed95ad1c6d78c471312b954361 +size 26852 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..b4db5cca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00b8f7d5c062ef851ac288c5ae4d25e2bcbe0c0cd30f4bb1f72e11ee1ef8a97f +size 27203 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..f9af9525 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03c47212dea867ae48276df731d4d1eb4709c9ccf08e8954f06af323d995bc8b +size 31407 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..4c17ed9a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:721826675a6aad20b88cb25ebe4f910775f8198a9364ebf8e6958f054e6144c6 +size 26992 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..031fddd7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:020e7fa2a0d5bb41be6f8c69f54099d9d81f9b9879299ff83710825fc7cc7914 +size 27222 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..982e559e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a7ab5c3c53dffd1b2ec38391e90acb2067dd752a8847bcadbcf7d1988ee6650 +size 33076 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..089d6cf8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a05101d1968a382050d74e7acd86bd5f0c0e35d7a8328d6e963896cba9148e0 +size 28978 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..c77f5af5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8337df6382b61725a927a17215d21101220dd4036bd8b312caad6c623582bb4b +size 29074 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..043bd5c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:895d62c40dc7f8f68fcb5fc1711f6df04eddcad648fd3ac94b6cde256b7a5a1d +size 21126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..fcda60a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b6910199539619712ca2e7849537ae23fd9fe5315f26654c2a652826ede1051 +size 16000 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..dc78c805 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0646ffb066148689ca717140f423d9800d36153d775ce57f1dfeacd2f721f22d +size 16579 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..9b4c180d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bad32c26616df1e0a15ecfc12b2a6dab2215cb2eb0a258052f63bdec38c93ed4 +size 21873 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..18f0608d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:087e75006638419d270cf43bb37479c8aa079119012269c7c91db57cf12669e5 +size 16959 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..d33c46d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fc15611bffbc87235fa2bdb56474eb314dcd9a2a6a4608dbd17f29aff5acfda +size 17578 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..eb47f5fc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d234519729ef8763a26d859d1eb572f5f6bb160a323b310eafdaf24eb8232b5a +size 22084 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..1d12ff00 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:531107bc15b21099e94f67d5963988473268f1241f71863d8d912d375a541b52 +size 17468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..6998a4e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:815d76a49e38297d78e3ab2f0d4bb5bcac87cd21d3038fe4324ae37f0ae25016 +size 17817 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..00f04451 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc8b6b605df27451462b0376198e6e82d0da4524396e118cdc3b6acac1cdd00a +size 23647 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..ceda92c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6460483d05eac274afb1ce87e54c66eecb5baf5922d0e225bec1ee71b8d2863c +size 19179 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..b7470fa6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchListScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad1007f9a6c3d7316d1972c3cf6d8fa74a489f444e15632cdf7161a78595c65c +size 19221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png new file mode 100644 index 00000000..f05df12f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c9eca3b46175ae8128a23237264e7e187c170a5bea5dc9e3695faf88353d965 +size 62427 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png new file mode 100644 index 00000000..ef85f3b1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenAccessibilityTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accessibility-null.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d22d0a6bdcea7fc7f7bd0cf9b3a8b033f0167a8ba19f9649bd2d99188fb1656 +size 63997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..33e0f763 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ec755e979e8cca0d4a6d5bcd2e5ad9f65b1452b0c976a6fe5e98851956d3a4 +size 34839 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..3c344308 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a5dd9781dbe12f9f9550f0d5de7e6b89621a319cfc6c6a88df1d9b18675e664 +size 35877 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..24db71a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7efa73e6dc4180c94c15efe957c067d6db736c122013e60ced63ccc30a2ba4b +size 39147 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..69a5273c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5aecf7d5d3b6febcc32ee60612c0a3d9e679c945543a97db74fc359bcfc33821 +size 33412 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..08bf9d1a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33c593159585b717b01948469c5a2d7569d1318525d2042d86625be88a501f93 +size 34564 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..50187bf9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82c7d425424ce9bdf3ae1a9a9450a310387cf34970cafa7aa025b2cdfe3dd6df +size 38180 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..f05b3c6d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a60a8b026a6407fd4552d9a30a730e93fbbaa3a0200a7e85e88a36ab96a6e6eb +size 39518 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c6f3e10f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85cf2f673ae8dd0150bc9d057f3ada0d2ce9305453a3e1db341eb51a7f32bb2b +size 42949 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3c72a2b3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:697b821645a56e819e7aae0024035b551a7e23a76e07c2238da39e43f7306539 +size 38035 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e0a803e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pharmacy.ui.screens_PharmacySearchStartScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bc30e3fc2ffb68c54f1f20f0b0ea884182385be155b8128c323a2b1d6ef49d0 +size 41707 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..ca6d8757 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bc61887824de7a08ae82f21b41da83ac3122e12d73f293961c742334e3ec272 +size 25622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..9d7eebd0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:255bf20c59e98727c53949daa96f0070608f3e33779a92b6de5311508c398784 +size 13368 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..2d017f67 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8ce908c115e59bc984ec4ce55a9706c467772b6dbae1aca6e1cfe3424a80d01 +size 28615 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..81a28a48 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94f9029489afca1671587aee4e52ac5cbe10b8551aa53bdc3d792c95210e6c09 +size 16120 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..8a8d8729 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32a852edd80e8e783fb2cb9ba4ca12630d40eabb563843bdd445fa3c06fd63ca +size 39505 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..8711e0b2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5e43e23bd9e91c3fff0775c39742a38caa8614dd0bd3e77134ec45eb75a8529 +size 25269 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..2f2e490e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ae81f99be38de521a4065d84ead53905c8f4b19caaafbc5e01f1a19f668f8fc +size 25173 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..290c5b16 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f147645de8b77e232ce358e54d024fc2d8e257ea9233bac8843dfe9aaf5bc520 +size 13351 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..419daa66 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ada47c5617281a72b70a325be0516ee262862ca9052a441e8c719d8c7cd9b84e +size 27803 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..16e2d837 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5b377d81550777b2eec5b298691337ef54e430b16b0e575884282f5ed2b57db +size 16008 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..3709914b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6f4797b942981b8f31ce70d8ac331cd9218e2f6e494ae33c49fb3fd36528462 +size 38524 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..3af1aac8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f9c240fdecce59583e2e010420035ce453a6f4132ef3e911ae9b1f5c1815337 +size 25186 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..a2f3f1f4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81eba0215b4dd7cda7c7f97ac2d011279f32528afc6d3c29ff8bd980c1834c3e +size 33096 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..c2b1df37 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4842816bb95c8b04bd7e276d141e9c5e85c66bf26b683266d748d81380c962a3 +size 18737 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..1c403fa5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c8c6d1d02885c5d97e075ac14c54eca4d465855528d6eafd71ca40d2b2aee74 +size 46984 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..cf4801c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9642d9c3bed0e620e751f95a1e807817d0eba24f9d533bf8cf1da3aeadedb18 +size 29101 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..9b4e612c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e065e493fba528c3db3c9c6a13b99cfb1509904987b56c149b9d957e0c04691 +size 32041 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..7c9ceeef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca1d5bbb7518e8dd2380f4e06c18f6fd50bc67483574b9ed6fc739bd60b62a3 +size 18500 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..4355282f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec20a8e7d11ff1ef4d76fbc5da796e005fc5454f48093f22cd7252ef688dfd5a +size 45719 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..f26cc600 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceCorrectionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37d47e8d0c82dd10cfa3cce25f2b51082bf73d9b4a6c8d52b48f9458e2d2118d +size 28940 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..f28a92a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d29aaaae608efcf8a96796a462cd7e66cb71802421c6bdc75c2a622fcb7e509 +size 78255 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..402979b0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0d3959ab15b0fca490c32196017be1f910ea8b8908a4a2dea6fec8894bf9b56 +size 78576 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..7bbbf5e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b76c6e2b9d4c4d18b55a1bbf0acabff3ffbfcf43bdd91b8c9e7e8dfd068d0df +size 79123 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..2a125477 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05ca35b50a8adcaa97659ec5d66b299fc8aab4fd0166f64be77d69040952b6be +size 76878 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..cbb085d0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05c644015bbfbf820f6b00caac48afef7027d701e864811cb3eac8253d07bd58 +size 77185 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..f36068c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d028163bd9cbab83cb93611e00045ab6f52d744c43fbf649fd939eca6b58621 +size 77825 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..01bdde69 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8606e973dceca177452b572b9a548d3ca9a54bfce8f448c6806b1ac5ce72438c +size 87596 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e5bff930 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95ede7454c951d51688ea273f05e1ddf50966f6046386f87166feaad2764822e +size 88544 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..e8beb20a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26d51d980e6c9b09c4a94b05994ea864cfd8e55a929c305d25ff0eebec119b23 +size 87316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..2cfbddbe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.pkv.ui_InvoiceShareScreenScreenshotTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24096c1a504a487fec2db017454d1d728eeb2c8ec371349bc1d6075e21a70733 +size 88111 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_accident.png new file mode 100644 index 00000000..1aca7b4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a03aedd939ea6e59a84926d364b6bce2e90a8a48a37d19a885031591b079e65 +size 17436 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_none.png new file mode 100644 index 00000000..1f5bed26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7543663a21f55affada8541d30f2445d66e63db695379a289aac74eb1293530c +size 12550 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_occupational_illness.png new file mode 100644 index 00000000..d9afd933 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078d9962a099f5ef0575797b61fcab6e1c6bcf86a2a201c2267d73f6f991a8c7 +size 17787 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_work_accident.png new file mode 100644 index 00000000..1d9dfe6b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37117cfb3f372dceb544675b9ee87b65238cd292c099467439b54a8312fcbfba +size 17582 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_accident.png new file mode 100644 index 00000000..89d2b3d6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:961bc6a32425ed49a32d50d694dff578785544de2f86a5bc4ae9797a31788472 +size 17083 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_none.png new file mode 100644 index 00000000..f974676d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0c8ae502836d78784f3bc0991920ebb22a2d3c8581f804c98590fa4e2b6450c +size 12793 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_occupational_illness.png new file mode 100644 index 00000000..0765378a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d46495b8de544b4aeed82e89e957ccbffa2340a5aadbbfbe25dc999d5ec9b0e +size 17913 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_work_accident.png new file mode 100644 index 00000000..ec916c66 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fba8645379d34575918e3c48721ac2e8df46527b4c8ad289e75695b0126a2a37 +size 17298 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png new file mode 100644 index 00000000..eb7433af --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e29fdc59c4bea1f38dd9dfee372ea869cfd4d8168d0cb58a0976f7b449ec345 +size 26659 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_none.png new file mode 100644 index 00000000..5aee04c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d213eb06decdaf942fe9fd33dadbfacbd9a42d7e4f3121f6712fca1351d9f52 +size 19923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png new file mode 100644 index 00000000..d6105321 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d17a413a973077d169e9a252425e091205cb8890af0e74908b46aca27f90556 +size 28059 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png new file mode 100644 index 00000000..ebf5a075 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cda198157019db93779f7593cd8e62b187e5706bb28e10b07387258117e8fe15 +size 27006 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accident.png new file mode 100644 index 00000000..41c21cc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c142ec27ddc7d26923a7c29db14f512c34e116b6c2ce85fd41c6d42c4a07259 +size 16698 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_none.png new file mode 100644 index 00000000..c4bea818 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb9048c9d15de49945126a201af772e705e626ded31e64be7c973c11aa64bc9c +size 11893 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_occupational_illness.png new file mode 100644 index 00000000..ede81415 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ba16137b076c276cab3b2a2b369ff8709141d59cdd0fe18e0810c158bb920b +size 17038 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_work_accident.png new file mode 100644 index 00000000..f358df39 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7813a77f524943870afead71346f06a065630b89717c1c3608447b6a2a921169 +size 16793 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accident.png new file mode 100644 index 00000000..eea12a9c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8989ea57da7ecc016e523f5f7bd429d2c7a91a82173b8ee23a89763cffddcb1d +size 16369 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_none.png new file mode 100644 index 00000000..a3bba3a9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c303d96ce178b4070d45b748ff93549a91320b40f56e66a7de50e97ff666f409 +size 12155 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_occupational_illness.png new file mode 100644 index 00000000..7009ff47 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80e6f8d3f05bf7c83958d5916ba7f3ab52aea44f280c00a1b54f7b7d2acc5eb7 +size 17144 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_work_accident.png new file mode 100644 index 00000000..29f778e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ba8718cfb22e6be5bb534567e3abce8cd4f05829efd675cab5bde112f766eec +size 16502 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png new file mode 100644 index 00000000..07e3fd03 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:351ab699c96f0ae231a98c22ad8982f97bbd3bb37f815576b1fae05904f53d59 +size 25768 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png new file mode 100644 index 00000000..cd4f19c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ad85413e41434aaca7dec62977f2f97e3100541a0f00484aee2dc4d9490677e +size 18770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png new file mode 100644 index 00000000..b4b491fb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8e71c0d0e2f5ecdd557067348302857020a04e8409320a1e0fdba2a82eae221 +size 27021 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png new file mode 100644 index 00000000..dcca37cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6dee92ea5a1fe8d69052c7305ae46bc8df79a65fb99e971b47963141bdfb344 +size 26112 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_accident.png new file mode 100644 index 00000000..9bd5bfd8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c95bb6a31520bd37620dee28dce2d018c17a7d4a5a78b5df6d845eb350e40f84 +size 19086 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_none.png new file mode 100644 index 00000000..dc95fead --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:440d44ecfee8759b49c031dc78a1f591be2def61a14faca8ff2130f9ecd19733 +size 14003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_occupational_illness.png new file mode 100644 index 00000000..39afe3e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5659dc375d46067ed59e6b1e82f1d75027428d131d6db8fd71c11cffd29b4df +size 20106 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_work_accident.png new file mode 100644 index 00000000..20c66d4b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b321c7ef9f167e5825e603e1b673b44c1010449b1546cb419b1ea54ec495f11 +size 19319 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png new file mode 100644 index 00000000..95647677 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebbe4f4c5589fdbfc726e5ab19d91d8aded99649e9d68905c41814679db6d44c +size 30784 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_none.png new file mode 100644 index 00000000..ed069b9a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2a9d06f41c57a606ab96fb1b61eb41761e28cf3c96f01283752b4ff4c902eda +size 22394 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png new file mode 100644 index 00000000..0351ec3e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94a89e89c8bdd392958779496937797ee5ad49b34139bd0c9c251e5dad15636c +size 32441 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png new file mode 100644 index 00000000..7674f6b2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4d43510ea6b975a75b635a587794596120541b9b1aa1de2e52b2ccdcdc6b407 +size 31156 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_accident.png new file mode 100644 index 00000000..0d596038 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef22f1ee7cbd9ec5a3ff2163a1e039fb54ebc5d078b59d89c601863b4fd30c7b +size 18349 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_none.png new file mode 100644 index 00000000..416c70b8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5143b27b918408bb5eb6d5e3e332ada9119bab51b7b0b8c761ae7fb63c400ab +size 13372 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_occupational_illness.png new file mode 100644 index 00000000..cb6b54c9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e24817583ba00f40883249f98369293d839538de38aa8078535e52fd5800b11 +size 19345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_work_accident.png new file mode 100644 index 00000000..0a0b221e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37c7d0d1b26524b210f0ae3ced5132359a74b8c4f0d88b80f2069b65a34b67d4 +size 18569 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png new file mode 100644 index 00000000..1fef725d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb059ee0b6489f1587fc9637a5218296d5516776baae70da57c21310963dcc20 +size 29026 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png new file mode 100644 index 00000000..363b2f7d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:628ffe783c5b4f5d3f7642012594f4b294e9c4350a6b779f2bb9fcf2f6262385 +size 21092 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png new file mode 100644 index 00000000..a3a1e8c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_occupational_illness.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:795a37ec69cc04e33e74496f29dffd12ab0f4e801b2f845f1c7e1c887a24e1b9 +size 30554 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png new file mode 100644 index 00000000..c450ce8a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailAccidentInfoScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_work_accident.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce7eac72c68b4d04cb559b0af6d0a51c41b41c3763137d6f8edbd838186f0969 +size 29303 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..56c1da8f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbbc9e32ad40da4efce4ed4a04678c0bd99659d8aa89318b54eb8c9efa49dd00 +size 63580 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..15cf1754 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:616f02cdb201e63c685162bdc7a651b19fdade9c57a36860b09414e78d604ed6 +size 62407 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..e8617c2b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a6e720fe87e021bf2c0f195182ada1d7abbfac0970004a25559b3ed590d3e80 +size 62427 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_provided_with_last_modified_date.png new file mode 100644 index 00000000..fc948b47 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da471fc88de54deaf61f0ab8aeead9f020adad57fdb4c5344422d84d99843ea +size 62863 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription.png new file mode 100644 index 00000000..d80cd1c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9edbd6aa9913575907de3602dcd7a480bad59e239d223a367fcc8f7adfb32ab +size 45451 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..a91375b3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b06aac49228ead485d684e0cae69b14fe6407553ce4a1e48b540a9a3a6547c0c +size 36072 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..15cf1754 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:616f02cdb201e63c685162bdc7a651b19fdade9c57a36860b09414e78d604ed6 +size 62407 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..aabf5ccc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c23b4200e1266e311dab1393efd6814bef0462046f87e4bc6b5af4ee900f85cb +size 66262 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..fdccf4a9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c45fa9463d2e11f6b086f6a9a0e90a4ff62e7010da6050cb20b7a2c31277942 +size 66167 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..eda458df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23018e223948d47ca9223c31dc68e46fbeb4a19b882df60eb55575002af5207d +size 69824 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..871f2fa1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b834aa5c764327e89620fccc6cce8b96b4471a99d6b7e1472d3287c5c57bf7dc +size 61409 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..cb218598 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf940c99493c4d336ec8f58a21314331aadc328a51ad29346e9c0923dd91d06a +size 63514 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..81941f4d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e3608d6dd4018ab4dc5599ae0290b4fb67fa0ce0072efe5d9436ef879ff5c27 +size 62642 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..cbfd9e77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e200fc865d0e21f0768214024e13ea9a0c90b544a4aa16376ed2f46c6d22ffb2 +size 62331 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png new file mode 100644 index 00000000..dc299fe6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8824a0624140987c83d14adcd4c3a9a62d8eb2f73fc4059d0b3ea2f1348e3f42 +size 63141 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription.png new file mode 100644 index 00000000..bc94512c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a82a6baa78437f5926c61cb70ac2213465a2e1c539089ca4a5aa445afcb76330 +size 45999 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..a5817d37 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dd0f437b374172e1339a19e716f69a238383bbfb790dd3593ba3ddc6464cc49 +size 36109 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..81941f4d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e3608d6dd4018ab4dc5599ae0290b4fb67fa0ce0072efe5d9436ef879ff5c27 +size 62642 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..dc2c4eb4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa28d6c21685f0469896ad3b755d0ccc4a606bacf4e53586f844ea15df1e7dd5 +size 66120 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..eb64d7bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:975b88062f1cc827d4256915f1737b86374de35034cca9f14baa46eacce2f8a1 +size 65255 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..482f1da4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:613b35c59fd3c33d6ff05512e2516ca6d34b32cad7266215424f62370b905ee2 +size 70099 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..ad8debc0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1818794cb439d784e68b32dac2bfa4b49a3d921eae7b9e55c0eb84f78eef4c9 +size 62216 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..824d4d1e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0207071c7573118b6cc8dc90492dacae8be64a64399608eeb9a3d42647e740d +size 71496 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..4a06fef6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:922a7cab1461f389ff0b6a3f6d092aa52f2f558ae835126873f7d9bd26a3f392 +size 73085 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..23e4108c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33303b43d272baece2f8f26813ddd39cef988998abb9dae92ff9b11c0759088 +size 72319 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png new file mode 100644 index 00000000..b687f308 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fda99c7c1999909fc75c7caa58175b73b0364e377a370bb72e501e144f01c31f +size 70503 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png new file mode 100644 index 00000000..4ebe5a98 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f027fc6aa2aa0da7138c1321239671acf01626b51c90c07d6577fd36cc694b77 +size 71613 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..a5e213c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9099c61c7f971450563a7b4ac8ef5444a83bc0f6150d7e060a9e3500b8a850d2 +size 64879 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..4a06fef6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:922a7cab1461f389ff0b6a3f6d092aa52f2f558ae835126873f7d9bd26a3f392 +size 73085 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..f47d01c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5afee43c2fa8e85ce96076d56c907c2bd3ade2ae97b5bfe7c23994e8d2554003 +size 74339 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..9a587bb1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e1059ef130f000a79e76a17085d2cf9a42a3db2d6cde36cf10b5c9ae8309f84 +size 72605 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..9c7e8aa0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45089ddcde34ea6a6b90e8c80d1da79145d106250641c539d04b7162851a19af +size 73138 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..49351afd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:debaf594f62ac5f2a2dab07dbbc2f407cda95e914ae831dae8b8924c44d2d708 +size 72266 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..48d55d00 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc1212ea3531e31f0ebe87fd0f7e16ed7a16069fd9aeac8a8e948e0d7162c839 +size 60041 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..40ce54bf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cef4348add34affbf9327e3c92384848d6f60ab2565548f8bdb06717b28675e1 +size 58889 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..e942671d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bf63c4c6857eca70e528ab1b443ade95cc9f457f4f333196cd85dff267fe8ac +size 58938 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_provided_with_last_modified_date.png new file mode 100644 index 00000000..52dd7f2a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d793958ba4f3e8bd3cdc146e8ba63f5c4bed6014d8770faeeeab92d6e5900fea +size 59366 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription.png new file mode 100644 index 00000000..129a1f54 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ee96d2078b2f32dc2a4f90938352a24dc58ec383314d047c98867e6b661218c +size 44515 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..10fd0a0b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70a4f9e403834f61afbcce39dfda84d6bf5bcf6ba77fdc79a38d5cbdfe2d5b89 +size 33847 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..40ce54bf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cef4348add34affbf9327e3c92384848d6f60ab2565548f8bdb06717b28675e1 +size 58889 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..e485f8b6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5561c0306192bd877b17952ff511e81eab6a8b003677a73c73752398b82f8835 +size 62705 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..a9edc68d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f31ca3e18809f1a455dae7068142fd50b64c7bc9dc6e7203766732836380f0b +size 62656 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..f5a0b87d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93672580799dfad23cdebbe8f8b3a75e68511219edd83958a2005535cf00c818 +size 67169 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..0e2ec633 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bd7c6114c192f11507d3cccc087a05c391b96d9d452e69493aa53c51182b216 +size 57930 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..91699642 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21b47eb5985aa2c5bd6da29378ee98efd4bfd8468effc531e12bc7573ca6eb3a +size 59972 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..aade81bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5078ef7fc241f3ecc1a5bb9898aca6be0f7d0f8edee3f1bd806d4274e228c24f +size 59143 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..615fe58c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b168e8eee450279faf61afaaf250c7c35f94a78d174b9c7eedd4f37bd53f8cb +size 58807 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png new file mode 100644 index 00000000..8d38e66d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acb99b074107f58f1b95259066e5474f3ee4d8083b43cc73463a1703c3f3af4b +size 59608 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription.png new file mode 100644 index 00000000..7094e2e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6397e8999e20bcb250533ee1acc77fa93d3e5843e047eff405d91ba59baf45b1 +size 45401 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..0a10c820 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59c8f542d3231313267d2768989b1c174e58c05aeec45f2f84550ea9419c0c93 +size 33926 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..aade81bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5078ef7fc241f3ecc1a5bb9898aca6be0f7d0f8edee3f1bd806d4274e228c24f +size 59143 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..7ee15db8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02b476e9cbd859d39c18e40f2a2cef9a4db5fc3ba51850f3fd73c6272c0f8065 +size 62540 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..35cd454b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a8ccf2cd934062de15dd0c43f487d0099cc9c378c8a9cd303fd89caeef0349 +size 61867 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..5ab3967d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ecb4228417f21b8c8ceee1bdb58a8ea5196c4e0ba45fcb7e5a76a25cf33def5 +size 67554 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..4c70b83e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d515553d1deff2edc2a06b408f10f752c21049706d29b2fde31d6a6e7f84a14c +size 58689 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..c4419203 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8992960327815b6108b85eafd75f5194c506924a5c65929c0309e8cd9ac2f29 +size 68263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..c36e5c55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a2c707e32e9efff1895f78f5f22dd7d653f3ce5b00913c24f7bb57c9dc5b80 +size 69841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..e2be6807 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f45388c7e5f2a14d568141928a38226b912557996a8f966600d7ead3a658198d +size 69083 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png new file mode 100644 index 00000000..82190220 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1b70d74a38bfbd9dcc2ba4eaf14cdf5bdaff62e23a48f9540c8a3d078353d83 +size 67257 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png new file mode 100644 index 00000000..390b585b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d389bc045d316126dadc4c38054d404e202ec7d1ba30a5a6a1207c32ad559411 +size 69159 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..95a2abad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a58430f62a3a3466322dfd1f092b7998cc8cdf240715d570643708aa9b4e0615 +size 60768 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..c36e5c55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a2c707e32e9efff1895f78f5f22dd7d653f3ce5b00913c24f7bb57c9dc5b80 +size 69841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..dc383e90 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65837d140acc89ec7dc3a909d37c826726e022953c13894571781c50a9239840 +size 71460 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..a2438938 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f4d54f5f698bd7d91ea1d5a1ddf62d0f61c22e61e2ccf8c6334e33e13ee998e +size 69600 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..0982fd13 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f70f95a3a2faf8a1de266cd1f7a7801c5d5d4f4ef8082a4ed6f153d00636387 +size 71436 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..ba373284 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:414fbefc2aa4ed26d4414daf5ad7498f434d8d919d72fe459f230d343769c4ee +size 69061 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..8fd6dd84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:269e9cd347a07f671a66e3aa99aa81d1246ad474a222b90c6368fc50b93ba9d9 +size 73256 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..8304672a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ef02d5b54886a6636d93dc4921a915c5fb4e425c93749efc9d3436c99cba927 +size 72160 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..318e88a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac79fd52c2f3bbcf33f8b63465a1bada4297d1c5fc3620f49dd84032632f10a5 +size 71712 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png new file mode 100644 index 00000000..8363db8e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4f6f1d767adb48c7ee9b967e13297f82011d68cb8de23cd23c6f30cf6ce2321 +size 72668 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription.png new file mode 100644 index 00000000..d1964028 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11bf99399bd2797131aaacd59ca7d2b3d1686802451b345e2ccdb22560f11c3d +size 52729 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..d96014e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5837b374ac995523eee7c3fed406d023db44e3c9e5b812d3c29abf2586fd666 +size 42330 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..8304672a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ef02d5b54886a6636d93dc4921a915c5fb4e425c93749efc9d3436c99cba927 +size 72160 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..865dfdcd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396ad8d0b049538253e67a811cffcdbe2fa6a1829ce7095f84f05c43233846d0 +size 73810 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..a46791da --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22b48406566c41f5b04af01985c43c9576841afcd64cb4dfd561d47f18b24968 +size 73038 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..b9b5bc31 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca963eb067614ec75bd3190118ec34f9b8a9b64046f825296083e7efadd06f43 +size 70376 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..081ba6bc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9acc7f14c35f1a56c5cce296a9487c645a1a717591efb0b541d7508a356f931 +size 71563 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..9b152262 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa29b2b623e8b3981baf3107225f76b5c7bea608dbc8d750edbbfb57c77b24fb +size 69641 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..d3de7a8b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cbcf5e0afe6c868fcc29be346671dbbab3c7e1eab58624b877bda2b7a600e3b +size 69893 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..e8bc787a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f76c2acd996b7f54a6112d3a955625b42f2d1fa2d495100c4bc74a884914fce4 +size 68932 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png new file mode 100644 index 00000000..1957ec1f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15f3a8713c88d3f3193b030a7c7d73e94f20574b1ef0f32755ced348cba78bc7 +size 68662 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png new file mode 100644 index 00000000..ab66f3a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05b57f07513301bcb70aa49c15a9f47ffdedfe8705998f1e51a9e371afd3d556 +size 72551 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..ccada05e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9379835ca9986fdffd5f833bff9568043b798b17bd6b4884c8347844636971ca +size 73727 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..d3de7a8b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cbcf5e0afe6c868fcc29be346671dbbab3c7e1eab58624b877bda2b7a600e3b +size 69893 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..45ba98cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3e7a3562500d8e404a64f0b5d084149583d6bfbec706edc5fc5537bc7a287c2 +size 72170 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..62e7abeb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35fe18c3a097d06e483dc1426ae6b5f19b823fbb1d30146b759bcccd247f6518 +size 73618 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..35b9ef41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3ba21ea7f33d686556cba32f5c9c139bc98d75fbdd3cfd9b8a76fb3a961fd14 +size 71135 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..b6eb4647 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abc6a3415162d5d97441fb1d90a94b71f806f5dab257df8a413fbb582f6212bd +size 68817 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..566be499 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f23dac7b12474c2923da3fdc701ba66a3fd356ac13c56d39b1eb9b3f6dedd5a1 +size 68841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..4a2a5bef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06af66d26f2a1a441dd74bc0c4f4f621209ecd85b7e7c300ba3f8772f641b2c +size 67764 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..24e42f0f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d89f2fb3cdd396503ec31a95d66ceb5059eddb5bb13ff7497d1e68680b7b0c8 +size 67294 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png new file mode 100644 index 00000000..6216cdd2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:868064a6dd97d1b53583915eddb717f289a5fe1fca4fa1c6d710ee49af09e228 +size 68309 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription.png new file mode 100644 index 00000000..7e244f7e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2e7b2932b9121b13d85dc2c4ae8603b61db69f240033b2185d46e1f175c3a57 +size 51983 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..2cb85a5e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c45752bb171ba86bf22c06d38b335c195975d28b8fe7ab16c31d21273be05e39 +size 39849 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..4a2a5bef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06af66d26f2a1a441dd74bc0c4f4f621209ecd85b7e7c300ba3f8772f641b2c +size 67764 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..15c7dad3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0c9760fa0877a7464b9d159661b0680d4411ea6b608cc3669ecccda0f087eff +size 69501 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..79c2e0b0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:326e6678a5343e3c4376f985568cdc6b0c52d84b0bc8aa3569c1d3d65885f6c4 +size 68713 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..175643a0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f89b913a7c69eb21df0ba174e0b2b661d445cc3fe2ca5a8c542c7c64142acaad +size 68211 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..98535123 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5247fd5e353e86c69ad9b0d67ec03d14cf4fbf3385e37582b6533d4197b0bc53 +size 67151 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png new file mode 100644 index 00000000..ac863124 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_deleted_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abfa13180e71b658ce6cb1b7c887cd2926b6abc24454146214905dcfb0190bdd +size 66446 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png new file mode 100644 index 00000000..237d7317 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5302a26b5668420fe35b96abf0b69b2aac096fd2e3594cae6819e45354c8cd1 +size 66518 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png new file mode 100644 index 00000000..7ea0da2f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_in_progress_with_accepted_time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66eca5478de1cf5d62298d44e42f735db5c9d8d2245659977e9a77b38942da37 +size 65621 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png new file mode 100644 index 00000000..91297143 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_provided_with_last_modified_date.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06d0579d311b05409f0e05c248030946d2207f829f927924cca95915039c9076 +size 65574 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png new file mode 100644 index 00000000..6039209e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:547783fa763d2fac3435d0019228afd4e0c5b08b5d8aa1edc082ac7eb4de6d13 +size 70517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png new file mode 100644 index 00000000..a75aa14a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_scanned_redeemed_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af7796a9bdbdedca41f5d781720fc1bf09b34b85a53d904e388719985a39b9e1 +size 68771 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png new file mode 100644 index 00000000..237d7317 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_multiple_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5302a26b5668420fe35b96abf0b69b2aac096fd2e3594cae6819e45354c8cd1 +size 66518 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png new file mode 100644 index 00000000..6315ec76 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_as_selfpayer_prescription.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc862ca82b47f656251461cbc5ef41b7262422a7fd1b79f760be0edaaceddeb2 +size 69736 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png new file mode 100644 index 00000000..2e7d5551 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:190d9ad7272ca20209c4265d8e1548ed43192ca5b75b5336df0a97309adbc163 +size 70671 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png new file mode 100644 index 00000000..db0443d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_synced_prescription_with_boolean_parameters_true.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d48832aebe47d540266e2e99e985a5127c3e2a529b3dea1344685f12fcf5a4a +size 69997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png new file mode 100644 index 00000000..989bed1a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.detail.ui_PrescriptionDetailScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_waiting_for_answer_from_pharmacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd5c0ad6f0caeaf0e0ec58f48aa0411b5b342cd68b00b51b31e806ff4f76ab9e +size 65450 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_ENGLISH].png new file mode 100644 index 00000000..5d9d7436 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418f98934fe2d3d04d7d30cc4558a62e05c6db7735288d0eb920295b35197949 +size 272915 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_GERMAN].png new file mode 100644 index 00000000..5d9d7436 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418f98934fe2d3d04d7d30cc4558a62e05c6db7735288d0eb920295b35197949 +size 272915 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_ENGLISH].png new file mode 100644 index 00000000..d1fb128d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52135bc7fc9de81009de8150cc79524f56a61536b5cbde172333a687f8151d61 +size 252090 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_GERMAN].png new file mode 100644 index 00000000..d1fb128d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_GrantConsentBottomSheetScreenScreenshotTest_screenShotTest[LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52135bc7fc9de81009de8150cc79524f56a61536b5cbde172333a687f8151d61 +size 252090 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..4ace94a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da13ae5cb56c9bda2f8cc6ee907a250a101fe19077c24eda14fbe8af0924ff2 +size 34622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..1f9bc571 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42c0e221ae5396848af15d29cc18ab92bf8b26380ad67365ed6b40460cd99d76 +size 50596 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..718d17a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82612c7f6f72f2072aed9e8cc864e972df97828b49632355dc2dc7bf625f7832 +size 50651 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..9a4056bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dffc0025ed5aa96df8a8d0cf33327a26bb0e22dface32bb8ae38af2eb6f65a5f +size 34275 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..c1897107 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1da6ed3dd013aa61c81bc88f9bcfae30f2c75769c33fec35ec9e5d35c7eacd9d +size 50079 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..17360548 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3ecb9af3d89f98329f4cf501e45ef8daeedade31476b1c16e109b9aac7cf24d +size 49629 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..3e6856f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cc2ae9ea43a9e30526e12e83d49728b2028a43bbea84741f729e6a87610950c +size 43783 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..fb2c14fb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8de7df8bc5ac4f2823d515b42a1679011819f225d24eda5a06acfd8a7b21eb9d +size 68950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..0c3ddf84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa512a6300ba88b33eb9e61579f019654da001e4bd2388782fd2bb0d70111609 +size 64115 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..356d179d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:222028b65528127574b141c7d36ca7552ec54d8122e43a51925ed5da616b8fe8 +size 32932 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..437b8f6c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:686e45622cb2a7706defcb44e1b4147da4ac2d25731e39f0532963a058702b6e +size 47678 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..9ae300ae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2e82b61a5468e1416363dddb97246f7f976d7605b49f8a5847c85f1b58ade23 +size 49482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..8ef890f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0866884c5f88152f069db56b17891e56048a884fef90018b542376f823e24582 +size 32658 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..6cfd523a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86e99b393e265cf43eaa55e7f3cf3adf1a1ef877480da90567270344709f88b8 +size 47082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..728093c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c75f15a4647af6c2812e82f6299541d4164ce6f0d065660b0f57c38f5173a502 +size 48377 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..3e0e133a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0350b4a284e852c5e75c4212e0aaff6214674a6dbacb89067503eaca22bdc07 +size 41993 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..cd53cc41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2cfa2fe51ac8a4a37ba965190365030e28d0ad33ca40c6f7df1d99661d9619 +size 65770 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..ab394695 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:590207889d4c5bf2243d8ba1d314f5f1ad712fce2f19081826c5de8c12fbb4a2 +size 62346 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..8850445c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21ab47f7d61fecf05afa4c2899e948099fc5f45201e5005e07abb431dec74aca +size 39322 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..b3d8f29b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3507a758ca495325e842a1ff04103470bf86bbbb62984f2d1dc93b3883134fb0 +size 57760 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..0c123450 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72adc684a308d776e93578617c3534e7d421e577cfb11cec3c00534a18348044 +size 57320 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..40246225 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83323980e9107cb010375f522a0f389403bb96c4bf26e40dcfbdfa510bf71031 +size 50107 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..02236532 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b6dae570142b8e4e2c333c92caa2ff4e64abc4b1885051f663ad7441982d70f +size 68095 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..ec61c800 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1c51688d3338093eaac14ae4a7a795fc9e711aa38add5582edac38e6d76c2c6 +size 64513 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..fbe61ba2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f97577a9e79a03da16288dc0880113b698df3ab45ff41f9b504fb7f145c3afee +size 37241 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..13d8f28e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db3b1ac26efce689cc37b2b8d35169f5ddb333bb7a5d8c98268a8c12487d1fce +size 53715 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..dd116dd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04534dcf21cc1eac65501ff51e0b6e05b6f0da83d7eae96afb8f2ef9a483be71 +size 54667 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png new file mode 100644 index 00000000..4174be9d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_empty-prescriptions-user-logged-out.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4611db0a3c68b11b2e949df69209345a112266a65fd999d987a29d2a5aaf9f22 +size 47803 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png new file mode 100644 index 00000000..a78595a7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-invalid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e7c2485d74ccbe5b1da5d243103ee17618ddf5014c602df341b919e28fd9dc0 +size 64245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png new file mode 100644 index 00000000..46f1b790 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_prescriptionscreen_with-prescriptions-user-logged-in.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42f0bf435761f0d895eba77e1425b36b02495bfcc334a44152494ceba6025d67 +size 62319 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..12725ece --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbca16fced30be3095e22d1573458362aed369b17b9eb4d00c70e537b9bdd4bd +size 55141 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..77439a7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a698e4239781ca26104da72caccec6aa97dff45e7a5a1405b7bcf6651e04d62 +size 15693 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..de588483 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0d5998889570d5a3f4c05cc624e219f02ea208f96318546d7776996a293b40c +size 49657 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..c8436bbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1e002e1c89636975433046d24107ae49e6bf99c42125b545cccdf0b731f204e +size 53392 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..a5ea5a83 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f8ea6b9f35f539adaf6cfbce6411d915fcf1775f0cee6b81bc7bc7eb40713a3 +size 17184 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..b75201ca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:613688d0b7b835fc392f83cdd1a0d74abf53c8fd98dfe9e6e4df188198495785 +size 49450 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..a58f96ef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f3c6413a7f0b37ef09b55b94b901579285b0da851af77d3940f354c83d704e1 +size 60224 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..85cc2e64 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77ff85e8c373e81a0c833303bcfdc9e7aa53eca6d8fc38fbdf0128aaee9dd9e6 +size 27471 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..c70c1d8b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18786dea6cc6b57652f80d2c192f12000ad53770700aca3850b3bef19b3cb599 +size 56547 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..a395f73f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52b801a0ac7281f5b9b1447f1c2cbb2a73b2fe7cd59521233864130637d1c0d6 +size 51083 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..f41b47e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e419d9979a4e27e0dc7ea1445139c50195917a6a009a0e85d1525b910bf07a4 +size 15275 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..5dbfdd41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a1aa40c57a191ec5368abb4fdd64974d9d818cb131b375a3b3937e32be53e32 +size 47094 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..a6634588 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c6058fa74ef04b17b5a140162ef4eda0e50c8281a474158f947941e747d6c98 +size 49348 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..e60979c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7aecd1d200a02cbaa5cc3ed8e5ff3dba68af51e943b99bc7687e493fa07fbb5 +size 16435 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..3d45d891 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3684c9480fcaa1a9ce15c9bd81c28039ac98edeb4d9f095d4b3816b1101978d +size 46821 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..970da563 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64c865f8bc0a70f7f6829e904591456288b869d73e226a68b63334ef8b8d3ff5 +size 56259 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..54ca7ce6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e93698d9a62336a98fee6b4d22fd5647a574153808cd43b1c1a378d6e552882 +size 26408 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..aa766e65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1968f337267376b03094aefd16aa5fec8a18c407a39046b5f88c2ae7877160e +size 54726 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..b7c162c1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ae85ac98971172f93585fecdd3b353fe7924dbc1c7c060256e3a78cb49af3d2 +size 59796 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..4a666688 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9619e4d3ae004024fd4c438e17ad4d230f4e894ea9d648703edfbe1521f3f8c9 +size 19081 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..0da89228 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79d895ffa5214511ab6de6d005a31e0f70d5c94d33d2171969ff989a6b614736 +size 57105 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..4d0420a2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:873cf04eba136b1c1f1a8097692e2be9c7f504ba921dbf711c0f66c84910e7f4 +size 67812 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..e19e1ce3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14945a3f3f208416a06e45114a6c5782a9d92c8e763a8835d50b859c1d453f1d +size 32014 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..66ab85fd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e1aab53a1269451f2b8d08a76974a962ba626ffaa9f2cf058ceab11dc4f0fb1 +size 59060 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..94c85ae9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fe8632c4d41ecbf647da82b782f78085557746979e723f73844ac4312f57a9d +size 54543 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..86f5a557 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bde5cff7f09b92dfd202844221900615ca1be06d59dc0a30a67f264beeacf90 +size 18401 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..7ec7b149 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27abcc467f750f4d0ac6d73c51cec25bdbc1f10c2a8e0751ca60c979bb913518 +size 53477 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..55e2fbbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fda0edf3c942cca78f389a3580fc58333c2b667292ab5ed1b4cc2209ce96eebc +size 62487 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..ff608f4b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5ca04d1311b4cec686e2681d2dcde2c6ba0f8e8ddcfc8a558ccbcfa7c49f656 +size 30257 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..154f417d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_PrescriptionsArchiveScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f2bcbd3f01facd7cd53b416b1858dfa9344b78de5855140bf8e94961961c84f +size 56544 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..a9efcb2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54f424b7ca0a5383eb063606f0a77bec530b688f16eea48b17bc1c5e65adaee4 +size 96324 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..a49c8de1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffb03e37f622481b822479eeec155d6530a981f802dba3c25d83ed677bbdaa0a +size 96849 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..a501b56a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2761728285147e1b88ebecb2be22b3d78c2491d75c21d34b7eaac12b5a6a18ee +size 110675 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..4c9042e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c49f813aabec0d0139aa17323d5b71d3e093f7291fca80a7ea08e6b63c5b5db +size 92024 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..bd578e67 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:506f58846775ef476562f86b212f5065846eae6f68034c335adc44d7af15ee74 +size 92433 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..64e46005 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4d0a41513496f0ae57b11983a3a76add3f15a4df9a36b01ab6d464aadbfe3d0 +size 105611 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..7b6646ab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f8ca4bb1ac4540b549411562d4a2663b12561a10467319160c6fdcd9f13a968 +size 103963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..1463e83c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cfa4b1754ef20eed8b34aa33312396a1282e6b94f69a86c62c94c523491f01c +size 121081 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..80e5ccff --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c3bd9601f50d7dcaeff6f22344c310800c1af8395b771ddf1731989d235aef5 +size 98550 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d2644a87 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui.screen_WelcomeDrawerBottomSheetScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e8cb92a1039bc1a61cf59e9f7932d5e952cfcb73a3d539f51cb2fe67b30332 +size 114247 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..83dedcc0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3ffc41d3ec6a6c99dff27484637b81eb2daf0df88b6065967011054218ed3f1 +size 114504 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..c6836c93 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53b68518d4817e59a0e45ae977af014df61053cf16e1e1e06f62085708382ff0 +size 7202 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..ab18b828 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8db3e62c060cc612b146c2dc32bbe486600f6483f20a2e2f5834f5b180e14b5 +size 7118 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..a7749ec7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9ffeeb779f70f9263229f82a0e25dee90c3cf98540ccff711e8d4ac6efc7808 +size 7640 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..8c5b3f68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4970c2939c9070e58e47f77fbc03802d9e57dfa0fbfe3e5cf443dee177a44b27 +size 7771 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..a9ac6b46 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ba47bc01f664aeecd6143e5934007d157f2cfa97f16cb1c25122b856ce0ec7 +size 7410 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..d12dda53 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111b98e8495af0c63e69ae0424aa28d71e361e24468e569297fb5558dae79095 +size 8697 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..e53e2390 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:856c3e3cca55407068ad5bcdbdba5e510821be190d9e06715bbb8494b5687184 +size 8704 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..857f4ad5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23be6699241e772b0ab78cce587e137644a8ea7d27ce9b719c01217c0c6665b2 +size 8930 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..bc7e919c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55e43e01fcfd8c8bb1e81b7a7c88ef28c49a8341050d57d238bf4579ec0dd3c9 +size 10304 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..e0116e7b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04f6918cc16ac796f42ae85ebffc63e8bd4b8ac921b0a865ff6c3098a6718d33 +size 9675 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..999174ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc514867b5865aedf8319ed2b3769941c4b158fd3efb29c69e5d51a578bacaca +size 110870 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..fe9e8043 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d3cdb488c6a5b92a8d611ee849895f16521e1e94f86968c540651fbb86fcc0c +size 6712 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..e13ef96b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dacf9901e357fce2bd34a0dabebd7dfc33f8b198b329e4a67e1bb4d23dba01e +size 6655 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..9f8fb42a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc5a007832bfa6898e19df1646770a3628c4de9c88e230a9ab3073dd2fb63f75 +size 7441 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..ca993f38 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:017d435d4b5f3312155f059d15c9fcb47bde44322406ca66ec08e424b54694c7 +size 7545 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..d31764a0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af6d319d5b63add0613579668f4a73679e8d5976d756cb04631bc330f564ad71 +size 7273 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..ccd28c77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9fc19e9ac854e288c2cc442654d3b315ee98e38bf58659c927b0d5948721be6 +size 8710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..3d4381c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:143dead4803d6a43625990c5000619ea3dc1fd087b0bf2c8492ca578b4928cc6 +size 8634 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..eca3f491 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bb8412067397a5d63478f08ce046517f0e2904f62252a1c5216c304785b1902 +size 9419 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..5ffb3eae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6a8facfd3596224875c88d854bc55158c39eb848e23c71afdb7aa532d6a95e5 +size 9207 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..37dad920 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b458e0ac2c886b4854424213bb7282637b2d5d2b04bb6e914f4283fb6d2b7e5 +size 9517 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..33dcae33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:976d9b1d5c51a2243b16274cda05424d35e9fa6679e395382779f9275ad01d51 +size 119875 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..592c4729 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ff0f59280e93a38b85644a57fa63adca9500dabf3cdb377b94372283f85554f +size 9370 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..c8a51290 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0553fbe71f175163904022c8c5b2669b42bd056b8662cb467852814b6249b2b9 +size 9208 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..29473d59 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a5e10b0f9cd16f593bfadb310174f06670d3119dd1f2b05fb9e7eb89d1d206e +size 10244 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..3c2ba9f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a15a6ac22f0a155e323692977b99b8b77f0fa67f69c94dc0a8398164f80c722d +size 10330 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..80655913 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:848cf476cb7b2d2e08db5ee805e2231fa1c5a8596fe9f2e9cce923ae3e57483a +size 9869 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..941f04fd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59345547eb76bb977b0dd65775d869fdda78cef2a3d182ecc7e90e0138a39344 +size 14004 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..77ad5bbd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19063b154bde818f1600ba0da4a5b2c13b8cd4ef3acff925e9b9383baa52cf73 +size 13680 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..43e2fcbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:525d9dd10107d883b4be6f82c73d4869e986c134d99c23c517c4d156d2da9e3a +size 10533 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..ddb5be75 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aff9cbdcb25818f0bc763dcece76167a1a8a11b7115586c814e332d38e07381 +size 10472 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..d3a154a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40aaccd44644781ae1c1e192e31da745d749082f01d1ddf0979eb90354f0492f +size 10582 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..c98ca4b9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b57e5bbaf3ed17fe86c3b122744614021db8a652cf6ae0d305f764188f315f15 +size 105869 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..296c7bc8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8e0db50b38c51c6e2b25ff5fcf6410005a1794e9616564bce9c1016d066cac1 +size 6998 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..9e1f6750 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4df2501eb88af18f9b56067796c295e03398ac7c8c7f6beb4d771930d9104ce3 +size 6854 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..3035a04f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d92a06bd8c2c1e1da9fb6f90e44869e7a80008646db5da62ec7e76d7fdc98e +size 7479 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..89b5d0d7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b326ea8600f5f9f6afd68d5a6684c4db4fd9a803732a1af0ec8bf86d32ae39 +size 7623 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..fae2a9fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9357cb4415e62bfb489fee3f2f3e9c7faeafc12f456e6e13c70d233d7d26ce60 +size 7290 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..f21a2245 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8489053033615cba8ee4f9f1eabe6feb27d9b04585f2e0f302037879d98c1adb +size 8151 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..1d6729ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e665275ed101abcd05309f345280c0339ff3b69e56d5312133ce218dda8e846 +size 8270 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..3a14c15b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c0319813e4a9cd0262187a8af57a6679ed27e459b122eb252967edd50ad77e5 +size 8665 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..b18ef594 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:520eae3b461236f66f514dd6d8b711ffa6c6bcb3efce8fbb8f7c8d9e4060ee22 +size 10036 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..c6ae45ac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfdad018e4d3a8c0a1b57c62944af2bbdd0550f5b2e655471602513e11212a98 +size 9403 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..161a427b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb0de2a99b024cf128dae433aa7615932de9270ef0520807279af117ff56bec +size 102623 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..c64d7463 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8436e76ac7c47eacbedee4bbecca01b19b86fad73fd80491184c20ae3b83fa +size 6572 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..f07cc573 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:877c0fa1d434300a24f8dead320795c378b308ff94d66e0ab20af1eb96dd8c35 +size 6498 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..5e15c9b6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d1f403265559734ee4ac5e61b30c53dca858d9565fabc6c7e595c825e57a15f +size 7321 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..7a391296 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1ff3faaf006411065f30dfa8cbb13eff017d38a85ea24f1852fbabadcf0c2d +size 7391 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..5e21dbcd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0645e119cc11ef8c80468d8fdb104a6119d9af3ec86fb7637e755c2ca17c2e32 +size 7187 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..6b8809e3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d446a30778bd1fe69abe09f922dd37be696b64e523ef117f7d1e7d7d5356153c +size 8294 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..446c5dd1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e6f6e29ea008fa5bface7f663d11d137ed71db8db14477e82dad6c94dc6334e +size 8221 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..0290afac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc139ce8b47a56e5bd99e8b49c36297eee9c25ee5c444c36d37f332b1f0cbfdf +size 9117 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..8ceddebc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5040d674909a4af957783e078b1f7a81590d14d8b6da34fb0f74af53c58a8f2f +size 8995 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..ab8f9ad1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15447fc2f75bf070e5f2e85b3625b7fbe9c0c587ef5e3d51af324ce6962d927b +size 9301 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..7f803edd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d698e1398a2db3ae37829b52b3368e098dd4a85b715120f9d351122ec7c05aa +size 111923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..100bbc44 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:826dd271b0c81ec16f80660821631d69838282f2c6257121d80583a68a6f40c9 +size 8866 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..29915ecb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e94f2e2fe9106bc1e5000829ac916b25b44cc9687912b33dfa0084595bcd741 +size 8800 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..4b17067b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df39ff77bd2dbd8d4921116326a22333170fc2eb8a9c32ea19d265af7ddb19dc +size 9996 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..c2a593e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f54d9619b4e1bc2944d9a904db7a9e756ee23ff6bdd33f6cf0de8db9b44214f3 +size 10139 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..bf3de5a0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37a3a1c7a4261bdf8ed1b2991844d7b380bb4b8333e3f21559a6eb8d3ec3272 +size 9633 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..51b9ae69 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc79da3fcf2af76c5b2a8f7526d159d4825466e3aceec753afd1d99835e786d7 +size 13179 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..3a23e729 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f73818a1eb704ed5b313fe3d364e107ea3b61a7a9f7493574019aff72e131cac +size 12878 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..73ce80c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c18f71cdef75858fff630a986abb6bc171a9dd6e23de513d3d26b0ff94015262 +size 10446 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..5eb812f1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf69a592399378be509ee438f9f4790c72b988b7748e01daff6e05caad292456 +size 10344 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..57e2cdd8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b1b746f1ea6ef83842b0469ed4f2d8ce48db96550b5d4b0de2071c4f818fcea +size 10355 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..2f81e1f9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffd1d96303efaee124d04350485baf226ba42db23a4afb4d2baad8b0667abd05 +size 128161 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..1d4ed7d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d25f9a3a331e027b4879a207aefb2140c2f09a8387e48b10295d2b4f6a8caec +size 7428 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..1d2b1fc3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4beb7334167159b419135f94697c2b955d2f35d399c1f40f26d76b5556023bc9 +size 7353 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..dc6468a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f35590dda768c200729404c480b9a9f683133e43152e06c38d6844dd5f066ef1 +size 8316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..4ad5427b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:136b4dd090604789570bdd8a496e59867fcd2b1df9fb25ef9f7c46a7fc1aef93 +size 8445 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..a2f9e598 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8cd3602f66fabf8ea89b7a8551a8230d6997c459ffb5516d31f54f41ea851d1 +size 8156 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..72115232 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ab4fa979c22253de8152a4a2fa049b0bce942f1a7617783fdd4a92a6c3267a3 +size 9643 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..d36c0088 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2df9b7b31faf02db369f0621da0988c85490251880cf9778efe8d8653b3c5b45 +size 9567 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..8b4e13b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1331026e72c5286d7960184c54cf6006ef0267f4c250028012c6b75945e3d60e +size 10641 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..a0fc3780 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a81939d2296ba8e81708b24dc43769a2b8b4c499bf2750694f82fa5956ed2a43 +size 10478 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..7159ad55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1da6c85962788eab26898b730d40942f02d51ee5136d6c2d0df355461b1b9431 +size 10647 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..430aa194 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04dba91ad1ea55579173bc6333c7bd16549bb0e3c0ba86629518422542081e28 +size 123468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..130b0310 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1897086c68c1d012447d183f862e79ab16cdfd20d140be69cdaec6dbe3353620 +size 10685 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..2d1f4d7e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e4fa7e266e4070669efd59a715ab0bc0a90de6e37179e0b36df059786e74f06 +size 10411 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..d4c104d5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:837ddf851393aac80f8a9032214742671c615eb718bf1f53386e916b7691c7f0 +size 11801 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..19810cbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:313db2fe4f60bdcdb4be6c831c11451c20ba1b34b3da3e83b8da4f9eae204c3a +size 12034 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..5e2590b0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae8d07e00a98f190e13f16a302cc61cc6bb4d2a1f985f3534c52da59ca8636e9 +size 11344 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..d1a9edc2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44b953ddab07a19dfd8acde1bd48e9a67ec55d7d6c1215fd96ee7525febfda3c +size 16187 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..00295bec --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbc5adceb7e4796c1c9f01fda2aeafe4183153a59e6bd9c78bdbfd1d43c3d456 +size 15928 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..1e7d7706 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:084a8ba107b1c2212f0193c262d876b20d4fb8716de1290933fa792b057df24d +size 12073 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..10136aa3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3eb0b840f7f0494608a58e996e15be9f8c9442756a0e6d98ec22168cf27d96fe +size 11882 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..146b20eb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9dcbbae2e0414e7b933f2bfc01a38f62e78a71bd7ef1dd27057f4c9888b145 +size 12011 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..13513ea9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:946cf3c7b20a2f734760b5a1dab0c4b694abca5ea5f5de145509aacb664180d9 +size 119116 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..455e423e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:670cc09a99b503cb75184fd68f0c889763ac87ddf2421a1e92d641ea1163c145 +size 7084 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..5d2056dd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b11b0ff4713514dbe310e31e224ca0a0555a410ca5a5e3e7b9982a4c0f0b1ae5 +size 6995 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..fdaec8cc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:065f43906456e74357d6c50639319f58983890a78879878ae4d5a8bf433101c4 +size 8038 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..98133b65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51ca19cd26d4bec35d8fdafc583e26e44234dd96b1c0df214bcc589851c08772 +size 8127 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..92674485 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d0963fcd6be29b33da8f58aaa7882901827204bb9388c8518dfa05f19a4ab1c +size 7904 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..17064fa6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad14bceeee6b09ff248e843f8ec77454a27e9fa6198df9980e34fbac2eafaf21 +size 9014 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..17a7e4ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cdfa3593808adf57b122590ea3f0ce3422e8a8103f40ee7440fab9125fee325 +size 8923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..c2187040 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a5b21e13f9181a6eaf6720126ec6d451ac813bb921ba69b47e5d45620eb1589 +size 10231 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..a97fe82a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f1f0f1dd74090423cb09a6be94ba63c7a8b830d1c2377fbcac4a3ab30426d53 +size 10061 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..263f97d3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f89f56e417257ab9bd10a8fda26ea99603031254afd0e84d093f99756bae8f4 +size 10293 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..f03bdad1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5f1e9b72ced952914a2856098a7e2e783b8c5b2b926a278d3946ee61e82a2a9 +size 115345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png new file mode 100644 index 00000000..e6d5a555 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_0_days_gone_(valid_for_27_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ba62b7af9488c450dc261079fe16c1da4471d288d234cfb69d6bd25bebb13e1 +size 9984 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png new file mode 100644 index 00000000..4bffbad4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_24_days_gone_(valid_for_3_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2944cf12fbd61ef7b8a7b31ae9ccc4e9e610f2913b604d202354f2d104c4cd81 +size 9769 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png new file mode 100644 index 00000000..dcdf2cf6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_25_days_gone_(valid_for_2_days_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e72253975cdded9baec911012704ed5234efb828ec26f4af6ef43a5a5c18b77c +size 11406 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png new file mode 100644 index 00000000..ce4c5759 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_26_days_gone_(valid_for_1_day_more_than_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d015ce07dabedfdbb16d03c091e533295f40f41f360d3155b97750e02d7ffcfc +size 11670 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png new file mode 100644 index 00000000..0c9f054f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_27_days_gone_(only_valid_today).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e546a31d22bb0381428372f92fc55278ce1589a91b14d243d6f777544c40d928 +size 10976 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png new file mode 100644 index 00000000..ead06115 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_28_days_gone_(61_days_more_than_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c609383c9b480622e432797e9414d29ffa43ada27422e26757ebd3bd63af410 +size 15036 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png new file mode 100644 index 00000000..68f39d0b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_85_days_gone_(valid_for_3_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ac76333ebbe93ecf96c0a25fa15d9ea5a25653309c569a8ccf5debe966b91dc +size 14830 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png new file mode 100644 index 00000000..b503e9bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_86_days_gone_(valid_for_2_days_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e73f63b3a800bbadaf006f3a75eeddad3e0ae689572ad595e528b6bd081b78b3 +size 11645 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png new file mode 100644 index 00000000..085cb042 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(only_valid_today_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e44884e8bc7f50f1ad8cd2d1a34567c16e525345b60e576a81659d93fa6937f +size 11529 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png new file mode 100644 index 00000000..cb0dfd13 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.prescription.ui_PrescriptionStateTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_ready_state_with_88_days_gone_(valid_until_tomorrow_as_self_payer).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c00945380175de65da7eca0a6df54abd2697ad7863531aefb1b10db01c015198 +size 11635 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..9fcc03ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:892d941c63062f0192c372d6ed97c32e0ff3c1ef5d5ee36ca62540c0d395f3d2 +size 11710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..fba380f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0ef130bdb97ada814f3cafe1a0c525036dc8e5cb713a86f57c0c0e32ef06961 +size 17132 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..02380f3c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899b55ab9a52d56ef20e5eb1cbd48e0685e5187d532b14700b4c63ace4f6a56d +size 14374 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..9fcc03ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:892d941c63062f0192c372d6ed97c32e0ff3c1ef5d5ee36ca62540c0d395f3d2 +size 11710 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..fba380f0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0ef130bdb97ada814f3cafe1a0c525036dc8e5cb713a86f57c0c0e32ef06961 +size 17132 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..02380f3c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899b55ab9a52d56ef20e5eb1cbd48e0685e5187d532b14700b4c63ace4f6a56d +size 14374 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..554b5aa1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a38e937e68892692885f3d2043c536014ce6906dfffba462f5967c0f861db9 +size 10858 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..da2a824f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b279b8551da06838c93da88bc9c96d567c3b969497aa329f585ef6855cadb716 +size 16231 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..e5de35a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d6c33d1d48f64d11cae8306bdd2ace81b4b6b30544352493e766113976421e9 +size 13699 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..554b5aa1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a38e937e68892692885f3d2043c536014ce6906dfffba462f5967c0f861db9 +size 10858 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..da2a824f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b279b8551da06838c93da88bc9c96d567c3b969497aa329f585ef6855cadb716 +size 16231 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..e5de35a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileEditNameBottomSheetContentTest_screenShotTest[LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d6c33d1d48f64d11cae8306bdd2ace81b4b6b30544352493e766113976421e9 +size 13699 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..cfa94844 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:500b4e31eb65fd51dadcfdf78c8aebb21e8206767285a83644737fe72850d579 +size 10379 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..a3bd01e9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d2d6ee2436964313a8f84392faa98a00b4b6672209d0724c78a3df8f5c53182 +size 15320 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..fee97b35 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e7d5ad377c7d3bf6421f95d1b4e495021ff817527413e5a48dff3729d7a7179 +size 82971 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..3ed45ca8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0119a534f580b507e7dab39235c78e8b430941144a995a57423c0e332b487d +size 14543 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..7049dd51 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a13647fbe66232adef54388b003469a049b2f99e08b71201d22a2d8aa1f84b5a +size 14725 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..3303e672 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af73607eb1a84257dafa942c3c49e73a44e1e47e8edf0cf2936ed2d892cd2c4b +size 39766 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..5e96a9f4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f7ca1b460c6c5f3cd8d33bbb49e3177085f051e855fea0cff626e58bf526088 +size 11026 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..d8624628 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2b7270be4d01fc744c696128816e759a5143b8af65a90e7e19b0c4dbb0674d0 +size 16409 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..1f718591 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b16a672916281e3c20875122c7c2d922796ab0dd3bcbec9f80bad6d4cf50fd96 +size 85305 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..9a8c7e16 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88ee99c7621c5ffda2c22478ec205dad53838804eb3feb86d78bea1839ae3c42 +size 16334 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..cb96b1f5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:049dd7c4ea3ea5c0916ba6360df07bf44e1676babf0dd54cbb4ee23af7bbddfa +size 15356 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..a5c1cc8f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f870416fd90a6bb72db78a54e97eeb88c2384abadf02b83ecfe9559b1e1789a +size 39133 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..5d27a6c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca4ce698be5f875fd8148cce21a3e34f3f158012bf911d1804b20f6c703b4dad +size 16266 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..05397121 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93ae7e9fd13e85bc53480781577f6b132171ff445da7a295bfd85996fe70abe9 +size 26076 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..f60a733d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:882e80299bbfe1f0401e26b5e4254c265f1965fc1625a1f5794d2a5530a7319f +size 97478 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..bae7af8a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df3afc9f0fd249e92e38e3a78cedf860673f3cfbe250dc67127979eabc65189d +size 25480 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..4a574fed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00ef30fbd2e7f236f7182b4e50f978ab30ada9f5a4f4ec40bdc0f0f3e0233a6a +size 22822 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..de259850 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e0a47a6827c7946b6cb448482c2d8b39f413fa6a0f314c81094627276bdc38 +size 70828 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..0a74c500 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49de68dbd1a1a3f161da474a0b563a80db59fdd07f2185fa1681aaa3cb1d7db6 +size 10045 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..510ee703 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb0b9ae69264028b303884fc4363f4a797e41a01bc91fecc482f2ea4e96c1596 +size 14724 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..50a50d88 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:852c03e3cd18b51d5d4f8e262c79977761bbc69243d7d6a1071ca8622d5f0517 +size 79482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..908ce9d7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09da69cb4b499e990f49f1bc60bcd099c4ada8402da4d12a32c2a43addfa57fa +size 14058 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..3c07ee26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09818ec99b4a1b2e7b5788684ea48e76b427fc68d8638c7582df4877151c7b30 +size 14219 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..40f334ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5cccedf9b04f347d807268a89d65a9834f5afe3a67da13d36a668c717904b29 +size 37521 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..8a72762a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84b6d6409e9616c1759dbb8e2a0c08ca6b66cd1730990593a813f677accce064 +size 10728 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..8923ee01 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e794d3237c68dd3111a9d90239f99619ae31e57f789a172da402e9b9cef4479b +size 15677 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..2d2df0e2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:494835462f10a27d207a87dbfd1732a900c24cee8ef4fe9db3f869403dbfa2ad +size 81756 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..9d9098c1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:107f2fcb0f604e989dff77910d0ca2e1fa0cf450ae8f09889b4ecd7549a176c8 +size 15788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..f2530e63 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64283323291e687b9d27cd2a92f9190d23c34c3793656b35e0bd0e04ee700274 +size 14885 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..010ff185 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29251ecf21bfcc01a202e83326ded45428208ce2b5562709561f62d936fef565 +size 37064 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..494d0f61 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebdafd9211823a36fcbd30336969ffcd932479eb44414550ccf988596575d46c +size 15634 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..f24a7ae5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a4e23ce85247e403bf1f9b417e042ddd644929de129132a641acfb3071b329b +size 24963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..acca689c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88f50ff93a03f7ba5059464cfe2b18daabcb2de0fa16db3d0d0698f96e95a77f +size 93096 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..3899081c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21d9d1b735b83ba971505baf0916ad96981f178295cff5dfcb9d759df941586a +size 24487 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..ad7f5cc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5132004500d577e0eef8899b75239964e74def73d8f2ef8f9c8e2802f80de3cb +size 22133 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..18f8aa3b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdb01fc15e2bc68c9f4cb48f474a51b79c530a0d69ce06d98d73df9d11e95bc5 +size 66989 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..86a78480 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67fa12ec3edb7b3f8837445af18da0e25b082461c938c85fe722ce70431c2141 +size 12482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..8ae6e1c6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92b40d8f2abdf568d3e7198fc9e41dc11d415b3f5619e243375fb7febc18f076 +size 18606 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..2197d00b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f87dea3f7bbf388ff5979eb33d52fe2bb44be9148013855239bcf1a541c08413 +size 95926 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..468ae3cd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e885aa4095cb1857dbe006b93e54fb4e0d24098237520ad00fd6c1ead4b50e53 +size 18569 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..9c460594 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5df022938f6ba057e1529858e16a8d223e16a69e725759b8e5a5676f4bd8ad86 +size 17361 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..2e2abe99 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d19b542bcd101e92c05deb202894dc920f2f3892fd08fedb719cda872f2ee04 +size 45980 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..0b42a5ee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b478ea513afcaec96ba07229d795dadc4d57609a589178d9d65abb377e5d6478 +size 17608 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..ee515fdb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:960d2cbb9931f417ab7c5d0e91b3126e869de5ace94ad6ab51a5ffaf21b29e9a +size 28486 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..eb020336 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cffe63318ea107d1771b25713ed338abe7fd066ec92cc720ff8560c3069cc42f +size 108779 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..2dfb0b74 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82281b9b20b5b59d620df9756eab063601f6f19defbc4eda67d6b4357cb94678 +size 27936 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..dd9a0422 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efb47b0292e540f3226d8b91590f233abcc564fd54ba97ec37a1e06f55b4c755 +size 24970 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..0125dc2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7673363940d98f3eb7965b875fbc5377fbeeb9e9819a2df90b72945c75134af3 +size 75259 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..eaa143ed --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b44df25e3afc1ffe0d44c2d0b2ff0169a61c37bd8182b8fb5ffda3e22c78f7f +size 11997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..e8c78a0b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cafc2c8e9f5bd588905fdfc5339ca01323e932497128b3fdf7ceffc7300b1b8b +size 17682 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..7a80055a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c65438c7348dadf514515c46fcff28f745278ee7e57c96c08e70642ba9428b3 +size 92131 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..48e89e57 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2b97caea0f97657f78c60a981c5f2437c791c13a7e62e2d7ded456d0d843a64 +size 17897 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..03642368 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bc8e343b113d271ea7e9ff72028c0a6daaa997808ac3f53e0a8df6e50df932b +size 16830 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..01081eea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb273f17c3ef8a3611832354db5443972a79f8563965231dd0ae7d62bc839ece +size 43039 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..f70cef4b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0770643c8f3ffca16f199998ae99d033fb9269ff5d8bd3ad5976d5237e8f6ba1 +size 16950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..732b3dca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79026feacdaf2eec88b29ca7847a5f8e2c3d25233ede8d29f8229383be9bdbdb +size 27175 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..fad9dd4b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb188115e26980da296e19ae77c7e9d106753e993c1f5ade2dff3c5679856cde +size 103881 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..333073fb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f50e43549fe66935a002ed054608f436059e166812c8086378dd5b3eaf001b8 +size 26883 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..c5cfae99 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f9fa16a84d95efd883462606d56f690c801c485ce4f95f5509d9ec1e341686 +size 24239 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..14c2f902 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfilePairedDevicesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc55dde9d51bdbd38702131848168c5562ff489991c5873b18b380c49fa03575 +size 70992 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..6f5c9d79 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32f830c95d49c404f40a79b7b69f67a87fbbf336165f66636eadf189b02142ca +size 3731 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..d47b4ff2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ceabfafa22d231e01c7a6f42da6678e7b9aa985a6442c330bdc166bad415d12 +size 11762 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..d47b4ff2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ceabfafa22d231e01c7a6f42da6678e7b9aa985a6442c330bdc166bad415d12 +size 11762 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..9da9bca8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaf883f86957a95cfa03b787742ec4e07c3b5320baa07bad3e71eb9a32cec8f8 +size 53736 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..8a47961e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecd994219d9e276786bebc58dc2122936e3f9be7706a0c34679b2f9875dd80d4 +size 53005 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..97c5427e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac1b5e7b85057e8067918a0cc1c36b4ea7da40a061b60d714d2118686df234d +size 38529 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..6f5c9d79 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32f830c95d49c404f40a79b7b69f67a87fbbf336165f66636eadf189b02142ca +size 3731 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..0598c8c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ddd388d254a1e3fc1aeeac1835cab0cc5dca6cab3bbccd9104f9edf265576ec +size 13975 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..0598c8c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ddd388d254a1e3fc1aeeac1835cab0cc5dca6cab3bbccd9104f9edf265576ec +size 13975 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..99c8e0c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf3b49f7ebd02ea0d9054744612bc046e8b7aea7d5779e07d8f27db6deaa4d9 +size 53923 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..898ccb53 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85da175680fc2c1f947ec776cd45d55de2c74254542cf5e3c22fd8e68aac8640 +size 54150 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..5c8fc4be --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce83a25cb3ddee443feb92000a6b6d72c9d42a93ce15736021231a3ee2527497 +size 38708 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..6f5c9d79 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32f830c95d49c404f40a79b7b69f67a87fbbf336165f66636eadf189b02142ca +size 3731 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..2414ba72 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ef1d3ddec6949cf06bf9c89df906e077848e9aaca9041c9fe0675bb647c525a +size 23455 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..2414ba72 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ef1d3ddec6949cf06bf9c89df906e077848e9aaca9041c9fe0675bb647c525a +size 23455 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..d133dc7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b132d36474f357e83059779cc1ce52e4a0ea817860d4ddf4f92352f4380cbc9d +size 63752 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..d133dc7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b132d36474f357e83059779cc1ce52e4a0ea817860d4ddf4f92352f4380cbc9d +size 63752 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..1abaeb57 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e7eb0d1ba1a4a76d38949edae1014a0f43f633d6d9758e77d03392ef0725a3b +size 48551 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..9a3374c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17f22de0b7b417bda4db96a5e3ff4843cab0729362bdba530760774606044c34 +size 3973 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..977ea7d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1feedd97528b321510dce8ac402eed93489aaa401370863ab6688a4c568dbdf +size 11068 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..977ea7d1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1feedd97528b321510dce8ac402eed93489aaa401370863ab6688a4c568dbdf +size 11068 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..cb5291f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d16b8d0535540a7e557d12fcbaca57d6aeac44da01a5922d06f0db590965002b +size 51090 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png new file mode 100644 index 00000000..6f0972fb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8087b4c2b8a8ededbf3c33b6a0484eccf879e0e382a3345d19510b8725bc7a0c +size 50475 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png new file mode 100644 index 00000000..868bae43 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:449ad9b70ecb96b6c4652734b742e7056c16b4ffa0fce33e20b8c1698d5cd1ac +size 35883 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..9a3374c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17f22de0b7b417bda4db96a5e3ff4843cab0729362bdba530760774606044c34 +size 3973 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..abafa03f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60f0fa6ac14036686e0412a0ebf68b66e0e768c02f47cfc9d7310f468a1d2be +size 12950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..abafa03f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60f0fa6ac14036686e0412a0ebf68b66e0e768c02f47cfc9d7310f468a1d2be +size 12950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..546b8bb8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2ab9da2564b059cfb7a5fcb46288b0e563f2a63914d0ec209c88f213b93afeb +size 51402 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..7bb7bc6b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4dab34ab35214328c0b7c421bfb0b24062b3d342242fa46080332f8839a727c +size 51628 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..02f9b2f9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69e70dd8f2e57cd9e0d092edef472673f93bba78cc6662777ca9188d6f38639b +size 36198 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..9a3374c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17f22de0b7b417bda4db96a5e3ff4843cab0729362bdba530760774606044c34 +size 3973 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..2c9fefe1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee191e8cf5563df5b120a46dac6aea7e2f7654374c2876777df04a2762843fad +size 22023 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..2c9fefe1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee191e8cf5563df5b120a46dac6aea7e2f7654374c2876777df04a2762843fad +size 22023 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..9eab9eb7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3aeef29a233949ecc3b261fa343515bf18c3d5b859765be39424c096251a95a +size 60484 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..9eab9eb7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3aeef29a233949ecc3b261fa343515bf18c3d5b859765be39424c096251a95a +size 60484 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..c237faae --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:318137824d5fb903aa865e1257fb6c1c6692a8355fa1b6ecb94a38e91e63857c +size 45220 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..475bdbb7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce462ab7d734ab219850df14b921a1f9f99fa025d23faaab5f21c529df7caaff +size 4037 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..6ebd8d8c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5fb93f257b11ceabfb137322e2b405b99396e0ddc02d94786bec30c6cdd019 +size 16108 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..6ebd8d8c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d5fb93f257b11ceabfb137322e2b405b99396e0ddc02d94786bec30c6cdd019 +size 16108 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..116dde6e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a76b5c1553952b4b4894d29ed95fda2c21cf90735f9e72ea3019c992846a1ddd +size 59041 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png new file mode 100644 index 00000000..bdc5d76f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e09f50bf4bc2c511847312d89f91d2f806c64d1dd68676bc7e89541be4eb49 +size 62265 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png new file mode 100644 index 00000000..a2f1bd76 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8227115eacaab1042778f9c42ef0fec20db650dbd851cfe138de6e299879781 +size 41769 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..475bdbb7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce462ab7d734ab219850df14b921a1f9f99fa025d23faaab5f21c529df7caaff +size 4037 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..f77680be --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:266cea241f35c2d1bfe4b953ec57936cc3c6c2d4e7ed703a65c69adf08d144ca +size 26850 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..f77680be --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:266cea241f35c2d1bfe4b953ec57936cc3c6c2d4e7ed703a65c69adf08d144ca +size 26850 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..bc009076 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d986fc73abea83446de36780e55539b2f7f3bcdb3f6a74d96d999c7cb952879 +size 69558 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..bc009076 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d986fc73abea83446de36780e55539b2f7f3bcdb3f6a74d96d999c7cb952879 +size 69558 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..5b3d716c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8557383f5c3c22fe4550966ce84eaea101119fc096ad821c309b9979cf98ec8a +size 52048 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..caeaaf28 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d661b0e879864d2fb35139c165dba69ac3e16f3c1d14871df60da53edfd315b5 +size 4149 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..50f21633 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0f84e9a0580e6ddd498cb63e57735ff2d5065386d17bf0dcb4c530d619f2bbf +size 14763 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..50f21633 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0f84e9a0580e6ddd498cb63e57735ff2d5065386d17bf0dcb4c530d619f2bbf +size 14763 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..8801ddb5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7f034a40412aaba5a9e411c329418d1462f214e0e7c70570abd08827afc9040 +size 56103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png new file mode 100644 index 00000000..ad5010ee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20049159d104f6e4bf8ea8284944a79d33d479b75847da855bcb0d548ab88927 +size 59131 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png new file mode 100644 index 00000000..d723d783 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfd8a3fff52ce1fd56f7177a6799263b8200bc85b35d9b6c741ca7681581671 +size 38772 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..caeaaf28 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d661b0e879864d2fb35139c165dba69ac3e16f3c1d14871df60da53edfd315b5 +size 4149 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..f0d588e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc0dce11f0166191251884ea7405e7c661c36ccec0f2d724535c3fecba48ff69 +size 24792 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..f0d588e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc0dce11f0166191251884ea7405e7c661c36ccec0f2d724535c3fecba48ff69 +size 24792 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..ed3b9d21 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a1db2bf3ff37b54b31883f989463d358f6a0f11fc6a2ec51a3de30699a1d40 +size 65871 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png new file mode 100644 index 00000000..ed3b9d21 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a1db2bf3ff37b54b31883f989463d358f6a0f11fc6a2ec51a3de30699a1d40 +size 65871 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png new file mode 100644 index 00000000..ee355548 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.profiles.ui_ProfileScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcca160c01e199a22cb189e1c1766f9ab7191e0778c6eccbec0777d14f8a30f4 +size 48403 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..8983fd1c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ac26f88f6b1640995350ace77acd1326f6734079e66acc7dcd2a0eda630de8b +size 146099 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..41f33d95 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bad3f5282edcd4e821ae0e716761869d41fab04e4c42a122155c4099b9a055a +size 146965 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..989ff788 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f2a5c35af5634f23167e725e040a9932881c5fedbfb868aaa6decc55ebe0209 +size 156625 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..fd3040f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43da5a4749ae3f539d821e2342f1c5e6d5bcec63f132c61ca59c6aa002660d3a +size 134815 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..e78c727d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aead6e8401b9a455855571c0b534b343463d70527d4ef866d71b11660c29bb7b +size 135757 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0e852ab7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6a2406cfaee42476e605ddb49f87542cf692bc2ea2d0dfb0c43c9e7005b22f6 +size 145373 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..1c769c5f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b285f07afff979a597d63dfa7fb9e8fbf8730e6c9441d45a6aba3ef3cf1f3dbf +size 153617 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..5298a68f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d75f226d960c181e6d4de0805b47c3d1ef97cb2f280ce3f47991298810f0dae4 +size 158994 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..073aea78 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5310f47ac8091f17da45acf0886777e771c718484c7a3e9f11b35be503c5793 +size 142378 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..98b615bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_HowToRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dce219ad79a15b803dda42a2e7976335889e18f7c7b7077c5373909f01932f6e +size 147530 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_emptystate.png new file mode 100644 index 00000000..a87c5c3e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de49ccd713557857b9ddfc636c3f2f00d1032a52151c2f17d82ce92c59b80208 +size 14812 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_errorstate.png new file mode 100644 index 00000000..a87c5c3e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de49ccd713557857b9ddfc636c3f2f00d1032a52151c2f17d82ce92c59b80208 +size 14812 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_loadingstate.png new file mode 100644 index 00000000..5e8ff4f3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c21f19f5a334ab864df08b639410666bd48406e911ee90c05e7e7b3b6b5446ec +size 5313 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..fa6265c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86f543f5ba637f9a1fa080cba8c1cf80c711cdd9bd8c495e295dfa99c35751c3 +size 25545 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..b065d207 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de874a31cdded31c8ccb64a795321e725daa30b4821980980aedf03164336e1f +size 30618 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..f136661e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a52e0ee0329369c793b02a39b484d47467146f824688c3ebc784022d310ca2c +size 31223 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..5d513a59 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aacf8e5f6d64358cd583be8019c032c2be10cc37626e9fcde06f95703fe05b4 +size 33979 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..944843c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c287ab9bfea793bc9be8774d1068064bbc0b722fd073f260c9fe18450c457474 +size 16281 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..9ab4d545 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e067204cb37c27ebe19635577ccb9b197671a2216dc9e2cdcae686433d0f0311 +size 21904 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..1678fb85 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ca7afe023f9dcfa9ececd50cf7ae98264df1feac60b31b23320018904844034 +size 21901 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png new file mode 100644 index 00000000..54cfb311 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9386096ca7f2b86490f0d0369867878c156742c7bb5f38b4922c67ad8218eddf +size 18020 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png new file mode 100644 index 00000000..54cfb311 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9386096ca7f2b86490f0d0369867878c156742c7bb5f38b4922c67ad8218eddf +size 18020 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png new file mode 100644 index 00000000..79b075f3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b8e61bd08c6ed7cc23d787c06998c5fc696beae259f3791763adaeb248f76aa +size 5395 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..2ff0ead3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a20bcda95b84fe7fb8ef178e2ae990bb67c27d93d644396e878d936e870c161c +size 26109 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..8efbd0a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70bb8e566cab6170618a6620738c5a64863e28ac910b58d17d915b332f40fd18 +size 31116 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..c621fae1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb199804fd1796ec8d0df58299b58f07d63adf98b851ba32ec7a4d5922d595ae +size 32531 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..a44c4875 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7097a3078b568b822a5be40bf8af83da0f8a23073a889b88ac94b22fc79f0886 +size 34785 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..f9dd29b8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c11f6b1658025fcb21076b7be753ce61991a26909b0691120a9397f6b05abe5 +size 16362 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..12e3e590 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65d7b70ef50443a3075127eae7356085cee8c91ff6b16c40da3369f4e14d31a8 +size 21490 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..2b4a6870 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0143fc2437660617c565daa972df6257dc93393a98369125ffcc9b5d04ae7b4 +size 21756 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png new file mode 100644 index 00000000..86136b20 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4078dd01245233dc043d489da9e978c1f9f9b2a72a2c74f0683f9ee1ed13f7 +size 29263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png new file mode 100644 index 00000000..86136b20 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c4078dd01245233dc043d489da9e978c1f9f9b2a72a2c74f0683f9ee1ed13f7 +size 29263 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png new file mode 100644 index 00000000..844039b1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5578c7a47d38786d195d84f15d7e28aa3a0c0a0c108b9f6d7b6e071a00951891 +size 6056 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..8d32a4cb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d42db88a1e8bb614e755f61310bf13cc3ab8ba89f020c3ebee49b4de95078f75 +size 35898 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..aa21bffd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e9f4eaa71217904010f06edcc9018d5a90cd98c209ef39f564b8a153c4a4e4c +size 40912 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..efef5df1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a922b4262ce8f5be362cb337a987b2b8a4b24d1012ab70dab451a7f78c9337a +size 43066 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..df243a35 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f224c25a03b5dc6143a79355727533e01074414c376c0b9daa908d34a380fccd +size 46864 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..91f142b3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:112e565f401f4d024d92f0770dd80f2119dcf260154ccea92ada3178db8780a4 +size 18754 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..fde39275 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e471a13248e3545427380a3bbdf245073bc99bc71a3e3718a0c70aa431226838 +size 23512 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..9b498b5c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb05d46cf5474550a12202af1800339804eabfcb31b9d802cfdeba6daa0dd5dc +size 23944 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_emptystate.png new file mode 100644 index 00000000..a2428651 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecec39e143a3175045b0e90128d2f22843c115996f31d1979a5649b1b0f6fae6 +size 14439 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_errorstate.png new file mode 100644 index 00000000..a2428651 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecec39e143a3175045b0e90128d2f22843c115996f31d1979a5649b1b0f6fae6 +size 14439 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_loadingstate.png new file mode 100644 index 00000000..39ed6f67 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3dcb17870d06f158c7c20369a88940daf8f086d0414943ace6295656d25f195 +size 5317 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..1793f439 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5807bc72d637a03d3f7793f29ac9f5e6a216b36e44454b933cf5cb7195087c69 +size 25268 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..9d2b879f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ea667ab15ceea2964afce631cb145eb77f98cb2182077c578315c4eaf17c0ca +size 29967 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..26d7ac50 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee6a52eb9091aafebafd87a34633fd9dc6ff22aca3c108c6007e7c5092e37bd6 +size 30545 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..b0d15b7e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f08f0ea77ba76fc67626438f2f5dc40ff752d2344e0497edfdad9ab8b1d0d8cf +size 33377 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..48b0a70d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f471a93b37512db7c8cec7a18832d6986aef8d53c6a5861ce1e9ebae9ef2861f +size 16032 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..534c98e9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94b52e824cd067a7627597aa5266b6997efb52db27eafe1af97a79f50fe07a76 +size 21301 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..f5a7bbb0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d7d31dbb7a2ffdc417cbde51e2d70ede85633ed585231da165c0edb59407634 +size 21332 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png new file mode 100644 index 00000000..96cfc90a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:494206acab046b22c2fbcc686204b456e45c4dd5196057b75cbcbda926112a67 +size 17325 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png new file mode 100644 index 00000000..96cfc90a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:494206acab046b22c2fbcc686204b456e45c4dd5196057b75cbcbda926112a67 +size 17325 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png new file mode 100644 index 00000000..4e53a0f7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:766e16eac36ce403d7205d2fc2e39e56cdd5cd4f09994a63e1066487376e357f +size 5390 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..0d3ed7b8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06fe6b8cecbc1d9e2dab02063b81f5494599a9adbc1610eeea8372b0feb96d78 +size 25848 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..63e2003d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45ca3f478ea252a7ec4bee29c62f649ede12fedb5d97188c34cc289c2a19a4bf +size 30412 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..a7966452 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60abe264da2f671c6bd978bedd13787d1d6c18243edfbaaf44857ed3eae8a25e +size 31936 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..53e8539f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:026ffb01d31f46801af1af0f7e91a3cbf5cba5ce69f35626af598f68cb845afc +size 34166 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..29a6dff1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b7a333e3598ed81afae04d211a0848a776a86903cfa2166ccca09f2eab346c0 +size 16103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..17cc3394 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:666dbf07d640fffe31f4c2a88495938ab6029d3863836ab6717e8ce9950b35cc +size 20906 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..39a69475 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f131662ff4c6fd69c89a312d245d13accd60b8540bc54f86965f528080fae2ff +size 21103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png new file mode 100644 index 00000000..4b9127ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1a7bfd677ba9cd16641711332e5b62d0d9460d3e82b6adaa7d172eaf7b901f +size 28004 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png new file mode 100644 index 00000000..4b9127ea --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1a7bfd677ba9cd16641711332e5b62d0d9460d3e82b6adaa7d172eaf7b901f +size 28004 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png new file mode 100644 index 00000000..8fdaff2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9649d5213b57ed0b04d1c066780c7c4856aadffecbcd2fc1283c9386b8ef3a90 +size 6038 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..d2ee60df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6db73fa59f97999bf131351b4860d557250b743c00e5e3c74214295630012c35 +size 35784 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..6dd258fa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d9459747bb1f41ed4c1c4c49f2e1c227cd89c5be19dd15f0ff24c22a2864968 +size 40415 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..014a9ccf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32f68d520c0081f29dda8925665091b743b635ce8bfb5eb847ce9600bd30e880 +size 42574 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..072770ab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1be5da3d5ec299f31f24e2d30afe5bec8633018da91e1f313d702b289116c01 +size 46757 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..1ff9f00a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28ff38386e508a64c4d8fda1cb4e35ae06acb77353f18b609f97022f1229a7a8 +size 18434 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..b9c7fd96 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e35d3654dd57c6867c49b16d4ada83f7a095256ded092b4474935eb892ea9086 +size 22860 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..268d177e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5acfea97776bb62ebe88b9e9ca076c40d57f33a484fdb7af9b56efdb4b5e20e1 +size 23304 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png new file mode 100644 index 00000000..0da5833d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe72c84c91d2fc701970b017512bb2236f5292bc4ee52f06a88e7e05d00114b7 +size 20618 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png new file mode 100644 index 00000000..0da5833d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe72c84c91d2fc701970b017512bb2236f5292bc4ee52f06a88e7e05d00114b7 +size 20618 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png new file mode 100644 index 00000000..469857f5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9228207bc88c28ce2f7c2470c7489f0fb5151a0dca6b2b5b6b550ea1e2f2c7e1 +size 5838 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..ed0b1394 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b780ab26c2a08dac68c6a9f8d0474271a787f2ca229dc8d226c0aff9a12f27fa +size 28899 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..d162c52a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f4d004fb722cb116cb1b9c55959ba2037e388ee245289801c429c788156969f +size 34532 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..9354f7e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f18ef5309610649ad7e2117ab271fd0f4733ac52df5f944d6c4fc8667280055a +size 36237 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..34290887 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c095c895f74f29d79c2b0a7c5dc9668179ed260e3e754bfd83f1d5665c50f6 +size 38941 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..a5b938bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65266cb748df10deb8560915492e733bcf41ceea2e94a51ef51f640c6857bbd1 +size 16863 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..62d174a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a507fecc0fe66b3ca82ac8a5e7fbacf16f2b127406fb3698f8b2e0423ff683e1 +size 22474 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..b8effb5b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cf9e3225c1a3e3deaba5a056ee05c7997ff32da72784a3fb81d92e2ac56400e +size 22677 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png new file mode 100644 index 00000000..e56ea864 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e715d0396b267835098e452fa9735502f78a037f77b4569b40c9a95f53597b7 +size 33736 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png new file mode 100644 index 00000000..e56ea864 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e715d0396b267835098e452fa9735502f78a037f77b4569b40c9a95f53597b7 +size 33736 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png new file mode 100644 index 00000000..903ddadf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5103a8e46214c9de56c8a5e7eb93b332d6531ab0f1d804316207969e6628dd48 +size 6690 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..6696e8d8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b32cc10ae0c10015314e0298355024e981cc292c78204bd66d6d595396cacd7e +size 39960 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..5082081f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b25a9cf8751d967c9c9f084d747ee27bf1c5a2970e0509064d849eed0ebaec45 +size 45698 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..28fee38d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55571ad7df23f6a87d79916af653dbb8e63d309df180caab527e0c319176a3f2 +size 46942 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..3e110b0c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7200add075ec27fca5eafc3c9b080e7fbe9de965067e59706bfcbf7b6cfc9c4 +size 48922 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..7035137c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:045758882acb0ab126b1f5e0ad980f3b39634f52475805a8960c255285aa87b8 +size 19689 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..414748fc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:836233ce6c65d537ba446fc8e3b6db89a1064474a4e8349bf40f73e57708b0e8 +size 24679 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..7a55de78 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b6a8b5897741b7fc92cc56c65a1915e9615e21655a4112858420870fd7c025c +size 25114 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png new file mode 100644 index 00000000..a6a87bc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56ecf33d4169120184c3dd1a2af607e757d9da69ef885ea23a0fbf227b189b02 +size 19824 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png new file mode 100644 index 00000000..a6a87bc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56ecf33d4169120184c3dd1a2af607e757d9da69ef885ea23a0fbf227b189b02 +size 19824 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png new file mode 100644 index 00000000..c0bf4a66 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f48e978990c708bf8de31ec18a40deab36c2deb42b11f415026e4aade3218310 +size 5841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..23a1b21e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03d6f692cbce41a04037230f68488593ab981a7d33d3ae81f18aaf90de031e65 +size 28401 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..2627074c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c50ef3a4f4782a725c79d0b3130fb658a5e10f33d7421799de56c46ea2f84683 +size 33698 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..35848c8f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b3b276da0b88fe856e6945c5372438b52724021b2f9a3c8e86f8e121518af8 +size 35412 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..9f001e61 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5634874e21bf8bbc28591cec9b3770fbe6a22bc6665bead8d272144bc055d21 +size 38062 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..f0b801d2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dc4552511959e0a9a6b393cec76ebd94d30ab183e504abc9f1a25939ad430a4 +size 16597 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..c7e102ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e40995dd5c3121cca41a4fcfae04e1f242a0a2daab1d16c8d811e56507c4fa44 +size 21746 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..8d5c7e5e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa0ecb10a3d4703e0f9fffb0baca86bbfcfcd7babc0c9cb710074698692bf579 +size 21971 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png new file mode 100644 index 00000000..b8118210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_emptystate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d57925a8f9e6b9f7094384179afb597f4683c2778f0c31beee34a1b4145ab44b +size 32126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png new file mode 100644 index 00000000..b8118210 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_errorstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d57925a8f9e6b9f7094384179afb597f4683c2778f0c31beee34a1b4145ab44b +size 32126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png new file mode 100644 index 00000000..471321a1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_loadingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49cbabb5ef4b72972b0e8471829ce76cd7b5d215e789da7ae52f44790a099bc6 +size 6662 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png new file mode 100644 index 00000000..05d53439 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3_singledatamatrixcodewithselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be88788cee04fc6918b6d6d8c8b74c6d87a2f91d34280b4db9a1acf2e9f0d143 +size 39896 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png new file mode 100644 index 00000000..03198fc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_4_multipledatamatrixcodewithselfpayerwarning_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d1e2a823ca5b657d0489d10773dac13cab9cb7f6f41fde6b433ba650ad10637 +size 45178 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png new file mode 100644 index 00000000..1bee742f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_5_multipledatamatrixcodewithoneselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:137be9eec64b79869aae84f7c3d157c437540c77ab05b3f7dfdfe5e540418a9d +size 46766 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png new file mode 100644 index 00000000..ff000e5a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_6_multipledatamatrixcodewithtwoselfpayerwarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d384f534d16f57bd79fae8450301dac55852aae3b5d8a0797f5d0ccd788ebdb +size 48897 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png new file mode 100644 index 00000000..a1a1c82f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_7_singledatamatrixcode.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7155a7eb791d9bc3b7eac53019672a59bc812dc5a1552d67e9b17784f38100e4 +size 19328 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png new file mode 100644 index 00000000..69b142ba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_8_multipledatamatrixcode_single_codes_false.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48e7df40d92d963b6c36e5097fae3cd3fd618b1ff04caba4691310d6db555763 +size 24115 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png new file mode 100644 index 00000000..ff15f480 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_LocalRedeemScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_9_multipledatamatrixcode_single_codes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae6de9947191f7181ea37bdc803a1be642c1f4ab78df5b0a433b706dffb70b99 +size 24474 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..ca5ff04f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cd93e37a4c2f3571ce2a94b239158bc32d09ef231ad28377eb4e158313c5438 +size 113160 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..fefdd3f2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e69864c0dc019c056da8e5404cb1fc295b2be3ce41b5c0f3e5ef1aed89d0ae9 +size 113174 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..652d099c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c5f2814117640b63f891d1342a97ca78ba69481b8d9fe8e917bef5baed38caa +size 111727 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..fb0d4421 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:694b1f014dd1ed7e8265ad3e157350c8766d704262cff7de1f684945aa51edc4 +size 111724 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..d5e69640 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:261dcf76920b6196d9934015bb822e4e00c32465c07e345047d9fb093952f90a +size 118341 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..4b496059 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:408cfbd24fc230b381d64a36f29769eeaff52cedc67f9a990d6ede657df551da +size 118357 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..b034c206 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b9f2e49d0329bd2f0730c7f51ea3e94c504eda1c383c118a711c0e94ea233c0 +size 104961 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..f1352c33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e93036ff69cd70376e6b79f6d3ff9d19922d227d972e65a8a0df446efaa46bbc +size 104977 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..b3bacf71 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41fc1592d5f64b6230e7e1172d754a2bc62ed33745882b5fd90a080786b2b486 +size 103674 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..de1f0e93 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:647fe95efd680d75354a45bf4c5d87d63f3f0e89469c269c95b2990992123ddb +size 103703 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..64054d90 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:766b24748b394e7331723278dfadef0dc7efe73f710f135cdf5c492a21d6dbac +size 110203 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..1b46d0a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdffa4d0b9862967da035cd312915fa6015376fa1ce5a49e3623b05572089c76 +size 110206 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..4cd9ee59 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:132a4329000c45fd96e78474b1efbb3f8af4c13f8131a0323757428dd7bfaaff +size 119578 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..b4c6e30c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e1a53a0d1b1531f950f6263234a77d27b914fedf1f31d647f9b0c41a882874a +size 119599 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..eaf5664b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f727e389771bfdb3d690049f318f442ad7c13e2d10fb8f25682de8ffe8d4adb +size 127558 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..5843cc54 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34b4952adbd0e38674a503f2c8dcbcc5c17e4d8c32aadfaf1007f615a9eb11af +size 127578 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..ba47157c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e8ed42ebadc967531d6ec08190d1ac1b68ab12d850954ace39c2907c9732d8f +size 110751 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..3b326706 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:894ac7fd2a028c1f29445b9ad8fe6cc1e55a1dd6bf235f60ae52d497256147ab +size 110761 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..519fbbf2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3df7ea77a8558766756fefc3f501063c2a7a4afbf0d2508aab6b7517f830b8de +size 118133 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..a82a44b6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_OnlineRedeemPreferencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64dfadd848a4490304d3f832587343868a0a6cd1ed02fffa74569583f1ea7446 +size 118142 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..d9e34869 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:398d2040af405db8fc81e95f7015b9ea44d038eb35c918f1c8206d7565b7f05b +size 53245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..beaf17b0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:669122a399e1e0ba2784f982fc59b91baac5dc457c89dfcbabdb47121302b32e +size 67639 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..6d3fc92e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d0917477166c2e66ffc1cc1c29d6b28d479473cc5ef72fe9768ebe23f32bb3c +size 53569 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..9f346f6a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4503fe2c08c421c30596ab14f9225553e05392517584c22dbc6479509bbbd5ee +size 50126 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..78cdaffd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d53f1df36b4c1811f93a0de1a85c9fe20eba84991a80a48b38e39cbc4108c85 +size 67599 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..38890d86 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:250360437c68ed91c7af9d6fad3f7f22db7d467f451e8d95e9a9c7a03171a0a9 +size 55047 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..9fb93675 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83bc295391942cc091cce55b7c726785b212211c0efb14be740f5c6e6ae5c2c7 +size 62509 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..8a515663 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ae2e700f40f471ae9f14e3644ca9ef39801e05683fb9c087cf9a880191191b +size 71262 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..1f3c6280 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff09dc00ac472aa8bf6f20d5af327060bb4c574d87fc39e296ce05283924fae3 +size 63137 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..ca1fbb14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e597400f1dc22b0f6c54ee50315dff20c3343bd54333fed416d0205f90aa3836 +size 50577 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..e8208dfd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a9cd4928a14f6110b3173c2c54dbfda358a60a098066ba5f5be6abfb5de145d +size 66733 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..fd13983f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b5d389167f0f85bc9aa536c0aae0c531076f943afaf412d8fc87a63425bfea1 +size 52977 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..ef56984c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efe6d33eedae83cb9dee53cb41f2f15c99e7ff7acb8974c970839f1295100d46 +size 47669 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..1b5f7f48 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fcfcecb6582d3a46fd16be1de3101bfc54b106031a16e10ba7af8153b7130a0 +size 66556 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..e7bed7d5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b6501025247c38e385f187621ced9c8a6907ac11a759a8a0f44e0dff9a3c2ae +size 54501 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..fe40126e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:debec16fa5c2d6bf27211d74fe12a56724d283a98b7fd4efaf12e14649ce4d19 +size 59894 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..3d752e41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2da8a8a0740f3182c06881014eefcb25177a63acf553e552e6477efeff0dde00 +size 71278 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..1953456e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899a0f320c6b732ef8cc1d800e759ff1b0bf221c47b6a1678cc3d0569d35b8fc +size 63159 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..9445b4b3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1215c06f2c549e918d175496dc365e2e66a178c9ac8eb25b4b0fe61a93d4e1ee +size 55188 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..e3f51bb3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:787a055131ce3355138913bd3a0ddb749a5a481062d2137858823f927da9b0ee +size 71440 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..2ffccdde --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b68b7ce9da25ca24ffa0f6b117a6ea12225114cdc68191bb87e43510a0c9508f +size 57358 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..fc3c0f3d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ca26b41cfea3075db26774e9e84e21ecf3d5ed05fda9946dc7d75f9fb00ee2c +size 69773 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..af185081 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e522c4f8092abf224394b88175c08050548e72b7e1a726f2173ef139401ca0 +size 74252 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..966fb11d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c574ee4bb9c3c75b2b042ba978dea8940d3447f2dd7de64d9b7196f717fb6155 +size 63858 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..f575f871 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92ab6bf1e8a51f75e75cb2307563d8de9f3e9c905e71dd815359fda2c18e680e +size 51969 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..cd4c8bd5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c65b8d1ee587213295ef946a0105b40add42b06818b611abf7658e8113687235 +size 70086 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..2e31b116 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45d7be4ad5fc54a7d79eef8de8d312a2e8981c778b8f15eee2989add046f9ae4 +size 56497 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png new file mode 100644 index 00000000..72bb9ee9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0_validshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f8afdbdc170b493db1c8e3446ee01ad700922aeade42aa33489a247e31012e3 +size 66685 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png new file mode 100644 index 00000000..1b83df8c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1_invalidshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6e6f007e1d943bf2f2a1d707b8a7cafbfe62884179659b422b8d1509ccca761 +size 74059 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png new file mode 100644 index 00000000..3cba4ba6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.redeem_RedeemEditShippingScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2_errorshippingstate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e714799e9cb84f7ab3089be1d3c07595bbadcf9145c5f3cd936ad8eb5c8cc1 +size 64226 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..02016262 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5729a80c62099a73b81d273547a6f0534dd19f028b6a3570cde5f18edb328a37 +size 33783 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..0b7ecc9e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af1f782f5455144aea0986d235f61174726ff13ef26ca49d896ac99dabfb2d8e +size 34247 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0519e6a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c82ebe78a4b52a205e9ced83a98118db57fd17f9cbbb152f8cb02221bf15ebc +size 60753 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..1d750585 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89bae182ea4f46a95b976c05e885f7a3031171e5ea654027e3d94e9b553d5247 +size 33161 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..5f88d3a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2177c6e599a5af02f99108b1ce0900c8f00f702f05f730e6f2230e5ba77fbfa +size 33482 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..e88ee99e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:093840d0de05d60e0249098c52654de4e939e78eaa77ad55b965b6234a9bae7e +size 60073 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..46b869a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57845378b5c6725808896664100051642342ecca61c695ae4bf6fa71b1da1064 +size 40475 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..47598f3b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f471419eaa7c497b000afc9b082de4e42797a237e000ad1f4aa5184feb8e575e +size 70779 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..a4fff12d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed5d2edfb4458b5a4e9670e68a81dde9ac6409cedc3e3ab805ee89397c52e036 +size 39461 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..09558e91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_AdditionalLicensesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc919c815a391c940ab1b239dcbd03c5b2c296215b53b2184321d5b1ddc57ff5 +size 70092 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..41ec3fbc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:008a6d3900afa150e6d322583545fba16bbd267c817b5baaff9a6ea2ff6da230 +size 86180 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..aeffdd84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8833a7bed9933a6f5bf18270871ff47d301ad71966a3a28d3b9528fc8bdcd116 +size 93001 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..d7784be7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52306cb9ebb6b40f07b3dea7ab1f4356dfc025e615dedead18a89ae8b663ce3a +size 87841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..7c6a7354 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b559d9871aa54ef5ffae65b57f5550fb0b4cb88cb5c986bb34d222acfc0a190a +size 84580 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..1935297d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f15423342376780bcd881b0ed2b313b2f3142232a4d425832c726031392edd20 +size 90884 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0498ad68 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bc2c50ed52c87d1bd6b7dcd924ff76f5b26f36fcbcc7cda34821430fcd6ee39 +size 86709 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..26b13f49 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4bda50a9639c2fa3cd6eb77d9e20e97061a249c862263d351122fbed8f2fc53 +size 111055 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..42e12d2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2024ff16fefae002f03396bdddbc327bebc9127f047fe13bc174d475231facf4 +size 81271 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..706b7f32 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83749e78bde423597d2a6c08bed1c8835bf9dbf1901e414395a1d90de6b08c96 +size 107778 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c925d96b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAllowAnalyticsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a511744227cc3d61a5ec654c646622e0f0db1cd2f9d7f838ec8e3505253fce +size 79951 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_biometry.png new file mode 100644 index 00000000..eb5cc5c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa9c24944e0e849c6a2be8c92621c76956adc5e8453c8d0c2a4f96a05f149d32 +size 18660 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_both.png new file mode 100644 index 00000000..2fcb48bd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7142d9cde3d758f67f499cd1da8c21f5bef88335ebba5bc9d559ab4531cfe4 +size 21810 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_password.png new file mode 100644 index 00000000..715bd94d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bed2d55abf27ea592f8bf5da38b71a99c522bab5d86ab3ddf374ad663334cbde +size 21463 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_biometry.png new file mode 100644 index 00000000..30447664 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9cdde9500b5b9a578893834a2df9caa093e6a03bacae4cfa3f4713faeed7bc4 +size 20503 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_both.png new file mode 100644 index 00000000..11df14a0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1bd05c60b5385f8f77ecb2a6cbd14daebdf4b9750fcc96f5ace97e3af4f6d45 +size 23227 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_password.png new file mode 100644 index 00000000..2f6d61d0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfa930535b6c1cf91a4ccd03bd950978e68dda6cd2bc0037766f3fb772ff90da +size 22864 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png new file mode 100644 index 00000000..0614e237 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de56050b629d4e99939203bb101fd5c07e9df93ea0267f94f0734794f8fa26e3 +size 30713 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_both.png new file mode 100644 index 00000000..62a002b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7077935d32f64da2476d2a1b1dd8f314c933e51f32e7a4e41ca65f2cdda3538 +size 35840 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_password.png new file mode 100644 index 00000000..0b4dc289 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59be987d7d8f35ff3f9a0da56aa4e528ead21d9cb90f4a7d19d33fdfb7222617 +size 35371 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_biometry.png new file mode 100644 index 00000000..88ed9352 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5422ef1aa0a36690b72f2d305b184739238ce316d54326086342560e9846330c +size 18261 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_both.png new file mode 100644 index 00000000..885dba41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d0be05d401471516a2a359a5b29da9fef0fe4b616ee0a4802bf3491c1c193e0 +size 21426 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_password.png new file mode 100644 index 00000000..175faee5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cb1e363d9f748c0774b40462e0fef73636a2a71ca1479d703e6e8f1372f8164 +size 21116 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_biometry.png new file mode 100644 index 00000000..96c1d9e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e3eed2dc19c40feac0eb2663b0c014af48d571fe4be78bebf55c23d757f9ee0 +size 19929 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_both.png new file mode 100644 index 00000000..65e6ee75 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9936a3adc808fe94741dad5ddc82a637e1c7724d28f18beb2892ccdd2bf531d +size 22682 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_password.png new file mode 100644 index 00000000..767fe534 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fdc3190b6716ef2ce541e9498aeae2ff4b7c7d2ee8803757265bb8541f34a52 +size 22413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png new file mode 100644 index 00000000..bc817825 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adf655e21d0d4197ac81c1a0014e1778f63712b5d09deff77261cfe53c52150c +size 29462 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png new file mode 100644 index 00000000..cdfc58ad --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514642bcb2d90e2cdedcd82dc1cab8830cad4e994721b2d46a2446276cc070b9 +size 34302 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png new file mode 100644 index 00000000..7bf82c5b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:830db0fac81c170da46faeed7baa6c6eb72855cfc133c334fbf93be171b9c797 +size 34052 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_biometry.png new file mode 100644 index 00000000..4ed429c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f6defd4065f761d41cb9dbf41da0a0418ada0b1a3799010a1c1c7e8704337cd +size 22927 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_both.png new file mode 100644 index 00000000..8f4f1c82 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8af88bc68e599bfd344f0a697178fdc7c5a691706c1ff2a0e2bdd1c22bef1e0 +size 25991 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_password.png new file mode 100644 index 00000000..a9e3db9a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bca94c2dfe87e9ea50f7dcfc8415da2555310a2ab93173a29904f0ab79c70331 +size 25559 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png new file mode 100644 index 00000000..7bbc3862 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d48ac1cd69f54e66eba00fa2abb96eb640e992fc55663b2f5d9e83bf9d552763 +size 36560 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_both.png new file mode 100644 index 00000000..3e5357a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9136d97130f490568edc03b60427735a3e6e47b3fbc2c11197aa868b0d3bbf0a +size 42512 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_password.png new file mode 100644 index 00000000..bfde993f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cc1e36370f44d7141e079c48faa3f229e2751c551649fee0d95062426dd279d +size 41839 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_biometry.png new file mode 100644 index 00000000..ba75926f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1301800ff2c5218a6f5169bb72646233155f41333d5f4c64132b33ea476a4074 +size 22296 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_both.png new file mode 100644 index 00000000..d4cffbd6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e98a2ee3b4461771ec259aad4b34684a9637ed0f63475aafd14d8bc6b51cc5e +size 25317 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_password.png new file mode 100644 index 00000000..3bd6cc15 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d54ed5bb5805610a6f7a6b6f699764ec39480174212303d573594649fb3b63 +size 24918 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png new file mode 100644 index 00000000..0798ac9c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_biometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a902618774cea1a7073ca8797f60a9008927f1c121b1e5f22f3afbc85ea2e76 +size 34732 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png new file mode 100644 index 00000000..af538dab --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_both.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7915e8236dcd86fcda8436d6b7bd5d2076999b556fcc786fb8e697d2761cf77f +size 40627 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png new file mode 100644 index 00000000..232824fd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsAppSecurityScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_password.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b260b52a00ecc57bc59bafb123382520e42934a01650ed0ce62e23400dd2ae44 +size 40160 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..4dd0f027 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba1e5a29b671467b7ed442f58e9044b0d2b18cca52970153f09297d211b256c2 +size 36413 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..3c94ce9b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b84c2a59d2cc99e7243ca2e68fc669da3308ec8abf920668fcc2afea893d70b1 +size 36405 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..c0ec5e55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8b62dbb7fff9666f338f161ed98aa7ab89b422a2724d1940306b533ec72163d +size 36238 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..c0ec5e55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8b62dbb7fff9666f338f161ed98aa7ab89b422a2724d1940306b533ec72163d +size 36238 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..dc19b6b9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9adf2a84d585b4080d5779606379459d6488417b8f6e806fb6ad56d22e31e4a +size 41714 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..8f25e47a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14114449396c91cd98cc26cb3dac8a87120aa1f5b495b119a3fee346543d21bc +size 41707 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..0a9d56c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f478c93e877713b6f9f808647ae4ba355a5c39ee87ab60caadf1a2cab417b071 +size 41545 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..0a9d56c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f478c93e877713b6f9f808647ae4ba355a5c39ee87ab60caadf1a2cab417b071 +size 41545 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..8a01f951 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a61c4b64cd242dbdd133cc851eb04b0069e33b5c8273fa6622c23449cc790b73 +size 59747 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..ad357213 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c166959c23d0f136b2dc31509b438263d60edf062af85d2e3be86c8e55480c4 +size 59711 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..9e311a84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78a48ad2a50d153f1cbe3e287c2bfacf793c17ab9c18db6b8f71cad5c37fd158 +size 59553 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..9e311a84 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78a48ad2a50d153f1cbe3e287c2bfacf793c17ab9c18db6b8f71cad5c37fd158 +size 59553 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png new file mode 100644 index 00000000..d8304de7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd18e10a115ea24591568d4d09c671585b5dbf24ef069e81521b403a19c16f2c +size 35753 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png new file mode 100644 index 00000000..d30c8b96 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ad1f30200634733b40a84c21b70ca202ace066df17e8e861f2a74358b6cf9fb +size 35746 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png new file mode 100644 index 00000000..385f2692 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:669fc60100a1323a8ab30aa6e20daaf5c88ae79967481446eccc66a6e04c8945 +size 35584 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png new file mode 100644 index 00000000..385f2692 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:669fc60100a1323a8ab30aa6e20daaf5c88ae79967481446eccc66a6e04c8945 +size 35584 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..b7d0c663 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0bb56599e88a4b77da9cef12931bad36fcfbaefdd01dca1f9b7023a16bfe372 +size 40860 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..838fb98d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28b0f4cba9ea61215099d609c94259c88ce2f387a15a9fc91ef048a85ccca13b +size 40852 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..aec952a7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8638287e3faf4b656ef91b247f55552df6052cbd21d68808d2080639c5017fe +size 40691 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..aec952a7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8638287e3faf4b656ef91b247f55552df6052cbd21d68808d2080639c5017fe +size 40691 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..b38d8005 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98c2b987ad7bdbd0dedabd0118d4d8a07db9258d723b6f9d2fcbad16bbcbd7e8 +size 58634 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..2d8d7dba --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e27e77243f4182a81210d8065b6f1e2deb74d715f400ae726576191be60c25e +size 58626 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..ce9a6f80 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2def5baceb5e0b1eb5230f9c8407bf8cba36055a71af225702aa71731fd9c9f +size 58468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..ce9a6f80 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2def5baceb5e0b1eb5230f9c8407bf8cba36055a71af225702aa71731fd9c9f +size 58468 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png new file mode 100644 index 00000000..2d798214 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00af353726110d71a7484496551baa408c8b92c0eea67a2ed1ffa01c623578c +size 48032 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png new file mode 100644 index 00000000..bcc90d97 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcd55930a5d94ad21621d891c8ed356fd88a73921e5553384179e7e376668152 +size 48018 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png new file mode 100644 index 00000000..8a8ad896 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5659c59a39d85b0c45e4ab46b652277fcfea479708baec0eb552b1be49c043ea +size 47815 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png new file mode 100644 index 00000000..8a8ad896 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5659c59a39d85b0c45e4ab46b652277fcfea479708baec0eb552b1be49c043ea +size 47815 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..6d69139a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4faed23ce479b6b93e74afe1627d1c40030660e41823986e06498b1e856f5959 +size 65163 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..e19ac229 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e51d412b6a12e8854edd3209d3921cc836870d10daa0b42874e609eecb4a08d +size 65153 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..8d1e699c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3227cdbd52c3a336dff5c4a8c145780f358d6db7be8158f304a49c09b8e63087 +size 64963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..8d1e699c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3227cdbd52c3a336dff5c4a8c145780f358d6db7be8158f304a49c09b8e63087 +size 64963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png new file mode 100644 index 00000000..a918c17d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c197b65e4b52afc3b290991da1a4d497bbae1f23dc61f5a9720062af668932f +size 46788 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png new file mode 100644 index 00000000..45191e77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c74a4e00d32c9bd48aab80f1c31e083cb169891ebebbb86635389ed11c72d4ab +size 46794 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png new file mode 100644 index 00000000..af7c02c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71dae088425446603ea894618c3dfba2dc4e2176ad65b248113928897caf558b +size 46591 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png new file mode 100644 index 00000000..af7c02c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71dae088425446603ea894618c3dfba2dc4e2176ad65b248113928897caf558b +size 46591 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png new file mode 100644 index 00000000..c7ceca36 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5900b30e8b422feee3b152bbfafeb3b68563b2c5ba8f92f3b1dd22be78ee4e00 +size 63628 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png new file mode 100644 index 00000000..76137308 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe2dc50848949357d5bb2dec1c3aa259743497615d72dcfa3f823a47b254e30b +size 63633 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png new file mode 100644 index 00000000..90a1ee65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8556853e500131e710c21272f7e2debc4aa1e8f97f2a2906a19543c6f010d02 +size 63441 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png new file mode 100644 index 00000000..90a1ee65 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLanguageScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_parameter_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8556853e500131e710c21272f7e2debc4aa1e8f97f2a2906a19543c6f010d02 +size 63441 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..5c188094 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e60a1a3249824db8558da67f132cba3f2c36f86c80af143fcd9e83f12bcdf4f +size 74187 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..52e59199 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d3d24a399926d49c64961740d454fa599bff7f1aec361e478be8a14c32cfaf +size 72809 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..192d0368 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc8f9bdd9dff155f36c038c0706d13356fcf1091089cde6beda435d028edbca9 +size 77548 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..524c8d70 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:addc0ae0ef4a4bb200c76cf6d8f4d9303edf62cc40730a826258acc4295a2783 +size 72918 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..c1dcfa41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:940ee5e3a3a85b38afe1f5e0bc55ea6de7f6b43a9846638064422e23c4356483 +size 71192 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..8b0a33af --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd45d3eba82d3953119fc7346ed816ab46b53dca64be4d7d9d4aab09cbf058fb +size 76622 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..69320111 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6ca1ef4e07ba492b0e9789a65680b158eaa8bcc30c5536a8732d75b9aa5dcc +size 81727 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..32ed34a8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e592d20949c0d7e0d57ab8a8bba91b5f197070c2fcaf98f2338062e5405872d +size 78565 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3ac9cc55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fce8edb7b5dbd0c2ea43a368fc1be5c3fc47aaa464c2deba792755bdc1591ce +size 79594 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..7cc44ba0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsLegalNoticeScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f926e1259b88348d3bad228220f763a36c19c0a3fb00dbeaabe8181dd10b5f15 +size 77415 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..ce97877f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dbaddd548b05c7bc66f9f3dbbf5924215426a4f81922ae8100be2c61fd275d8 +size 100400 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..01a623a3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae82cfde425ccac515ea9ac5c0d044ed4c1bab80f0af3b4a56fa174af2df9a97 +size 100401 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..89e34ba4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b741062e915be027b218d4c9fa465016a1e7448fd7381da7e039d27fdef7854b +size 96324 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..c66e3c1c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb30214f96ba5a0d85c6f4fbc549b3ef3fc9f9522e4134d3eaea4855b9dc74a +size 93946 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..3fd2eb14 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:601ce16eacec138bb0250b12953e41bcb00a10ab448310870e6f40af170af323 +size 93927 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..1b0fec3f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f573425025e8deb0c0d888523b5e82c68ae334d1b4ed965798e3000b81c12320 +size 90379 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..6b948f1e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fad4569ef6b418a77f7e40a9527f9a813e8aeffa509ade9d3bf8b992320a48b +size 107167 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..8252a0d6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ceeff4fc27ba9f553351d48dfd5aea73677506b391a07ae9ba72b7387d1fd378 +size 102353 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..75fdc28c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfa99c138503d22f599ffb9680d8194ed649b21286f55a5578d8991221245c41 +size 100174 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..888daa20 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsOpenSourceLicencesScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6340f093c690ad0f1e9f100f0cea62110fbc969572d8984956f6557f00080b4a +size 96352 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsallowed.png new file mode 100644 index 00000000..01cc625b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7712b3bdc502144a483bc438f520fcdee206aa4b29380ea4c1eede10178d25d0 +size 24176 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsnotallowed.png new file mode 100644 index 00000000..0687854b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2254bfc05158ecbdac37fad87dcfad2182a4872174a6c7e26f442f612351be06 +size 23963 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsallowed.png new file mode 100644 index 00000000..a02cdb0b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:640d8c9a219e5a8a3d178108d1eeed13960317b470dae0808c610b89351bc383 +size 27505 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsnotallowed.png new file mode 100644 index 00000000..1121f148 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64ceaa4e7819fb8acae493a3edb74e939dbe26270553dc5e24eecb877c8316a8 +size 27306 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png new file mode 100644 index 00000000..c314cc41 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d652d869908c6a17efd894ff7001638362ca5e0090d271c1787647994bb15aed +size 49989 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png new file mode 100644 index 00000000..d7faa591 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc6843d07c90e64155611cb421e37608bcf27120ff04f96f146935b28dc0d252 +size 49727 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsallowed.png new file mode 100644 index 00000000..8bad2ffe --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fbb956f9f93a4924b03436fff79d7f7f233b3bd291816897c7f5fd93abf0e7f +size 23209 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsnotallowed.png new file mode 100644 index 00000000..04bb5294 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fe3dc249309ab4c9b8552854b417eecc68698dfbf8061067915800cde27e34d +size 22950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsallowed.png new file mode 100644 index 00000000..bf10cc4a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a59a7c7568d7aaaa3900de3b810fc7b65fc319700aa803d3d723eeda49b552c6 +size 26233 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png new file mode 100644 index 00000000..5f86a470 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e10f7cee1a2a5aa4a5d5b9b2d7e7c348b6c13425c9c9d1d8778fc86f780bbb20 +size 26025 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png new file mode 100644 index 00000000..a1e5167a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:296e82194cd6c75de2d0421273f7b05688dd1f5ff4e4f94b1cfa339418b1591d +size 46570 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png new file mode 100644 index 00000000..d568e00d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dac59e7195982913a20b4a4d89aae61cb188177cc53daa78d0441d7bdfad2d6 +size 46260 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsallowed.png new file mode 100644 index 00000000..c27e7ba9 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:660c7911c250d4573c5b02118e869b915fa7dacc6894d30270038c2e393601f4 +size 30871 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsnotallowed.png new file mode 100644 index 00000000..9836ece4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:958dcf3e1c4173e70648808edb27d7e289fd9b67b1ea6df41aef9f2a377ba9d1 +size 30420 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png new file mode 100644 index 00000000..1eaad704 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1596db7c25978db275eaa94bebd88aad7be67631bdf06b2f2b8a752b7948e494 +size 56285 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png new file mode 100644 index 00000000..6698cecd --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:accea597173fd2b37920111eff41821de5805fedf29d5a8f23d22917bce7d54c +size 56055 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsallowed.png new file mode 100644 index 00000000..3bf7a9b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3da3b816a911dc9df392b478a130746911770bc071040614b40e6c53544a880e +size 29351 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png new file mode 100644 index 00000000..1690f915 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e758cc86c2555f223e7f1e9420aa02f136c75e352fe9574dc0f0f923a19fd78 +size 29076 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png new file mode 100644 index 00000000..d1487758 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b35c5d08d9f88263aa677820a697ec58221ee164508a40b8e6a6897d8cc2a716 +size 53142 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png new file mode 100644 index 00000000..e118f10e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsProductImprovementsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_analyticsnotallowed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87506a38159eec30723b359490c96348879d489f03306a37bb458e4326b2e2de +size 52681 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_0.png new file mode 100644 index 00000000..ebe0ed76 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62d633167ded9d5bcf9fb0944dd7448b3e09f77a249985d6abd0d4181e8c45ca +size 48734 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_1.png new file mode 100644 index 00000000..ce518ea4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea5d60756935ee892f8c68b4aff7c23440fd29a724645f00204d08b52db0adad +size 48100 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png new file mode 100644 index 00000000..da69a7b1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54ab631ca77e96fe9f0fea1a81646023d4231a372b6e928999e69f50d28666fc +size 53259 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png new file mode 100644 index 00000000..ebe75cc6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58694a511ecc7dc21619eada4c4b3bce7b05275e624759aefbf86926dbd9fbe5 +size 52612 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png new file mode 100644 index 00000000..63c99c79 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16e2d3c527eb0e90f468c01171e1ffa3a007052a34d5d477c90e55cbaa6207a3 +size 55912 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png new file mode 100644 index 00000000..a040c194 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff34502b0a3e42afb6721e413b4412be54d646cb93d3d4676908288dd2c36840 +size 54092 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_0.png new file mode 100644 index 00000000..5e03bea4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bc8afe4e46ab0decd507a0c98f0d96c6e99adc75abf65887bd61ef429fcb950 +size 45935 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_1.png new file mode 100644 index 00000000..22526123 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e59dcc52608092bae5c87690801f523c79c45828b28629a0c00c94730e1001e +size 45501 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png new file mode 100644 index 00000000..5f7b9bd7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cac66a28398157418109687571dc17341e0c94cf0ffdb836f5ab40b3a20e705 +size 50090 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png new file mode 100644 index 00000000..fca0bfbf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2c65a472d607e813cd3b1b86dbd66e13b1a3311c27f058ae4a9f72f5b2d8f6b +size 49677 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png new file mode 100644 index 00000000..c5c386a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaee7b85b8c101d3f65d11ddc304fb2d797466b4156cdd92d666e470a9185cd9 +size 52794 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png new file mode 100644 index 00000000..c777c72b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b03b35abd0479a18a83b546e284a232458b113b9e503a8813aefada2e69860c +size 51044 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png new file mode 100644 index 00000000..4ef3e0e7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a000b4a27a49764ab4abcade40015046f49f8b7126c24102634327c13e8bfdfd +size 60613 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png new file mode 100644 index 00000000..8465fd95 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ca59e765aabd0c6e1a865f55d2fc58fe45b1eaedbdc68da25e3847c37f57afa +size 59912 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png new file mode 100644 index 00000000..ed05ac89 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c360da7f79744dfd7164151cd8e71a8c1ae9c9cebe1852c083cefd047efe412 +size 60664 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png new file mode 100644 index 00000000..9297073a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57dbf2ebe277d0aff25489255582c91ed0a341287972872f90446c8c1977d822 +size 58538 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png new file mode 100644 index 00000000..3800ab88 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3196731b251ef29b571e9a76edf10e88662cfe3305aec9f8cd60ae38ffcc712b +size 56567 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png new file mode 100644 index 00000000..e63bf84e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2018e8198dc92a5ecf089fd2a2cf567b5d9a61adda83876143a672c79b84f810 +size 56129 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png new file mode 100644 index 00000000..8b13e979 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59a3ea174600003274088d89c2dedebb294b57b73f23655c2349966c0b2e4a8b +size 56959 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png new file mode 100644 index 00000000..b181cf29 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_settingsprofile.name_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:162e063142e521bee5f944d07b0c0389134ea3242fe2c6260b5b34b5c71c7194 +size 54719 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png new file mode 100644 index 00000000..a0beb2ca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f619dda41b30980069a4a1ef5f939ee292c1e947c1a82e5907bef48e9c03234a +size 27652 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png new file mode 100644 index 00000000..179d7477 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78e2b4abe57f175a7d0eae67c67814e325e453f3e22ced95675334713910997e +size 28043 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png new file mode 100644 index 00000000..543df68f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b08fbc0cb6d2e285ef7ea4bc5fb8508adbd6b5d115cc9ef9a45b30d72da2440c +size 28833 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png new file mode 100644 index 00000000..f7adb6b5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5867d762b2c5493bc603f124e80a419131b5df00ec7169a670bb47920a9468da +size 27486 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png new file mode 100644 index 00000000..2d20e455 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06a7456c05f528b22f1789894264bd2270dbec59c05dabf0df4cbe46f990dc0c +size 29979 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png new file mode 100644 index 00000000..26aadde4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2e9d77711cd8a8d93d675a622d0c1859b479e6d86f417d0b296c76192bac456 +size 30532 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png new file mode 100644 index 00000000..9a3166ec --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc995cbd24b796b1392f87c1ad6b2a3410bc5c9d4937733b1161322c6ba9c687 +size 30946 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png new file mode 100644 index 00000000..4c61a79e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:335bfcd4b41e8cbcefaf39e745d8c2aa7b858c6dcc1463a9d4c7cd951acd27ae +size 29676 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..128ce691 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ebf78677b5dc97a7f34d796d550933d1c78a96a184b4d6642b76eeec12b06a0 +size 51299 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..0545ef1b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2947355506d67a61b99a66e1d636a7cb0191787c7481579e9df04de1b5a91ba4 +size 52070 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..28fb1779 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d377532ca3718c325a95005d326fc6a442f0e47b3175f41b737abca90997fc7 +size 53047 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..8e015dc1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:491a3d9aecf7f5eee258a6cf9541fa79b01a51e04105b68052a353c7abddfc68 +size 50601 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png new file mode 100644 index 00000000..2df12e77 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e5d5391963fc021140c08b336c77aed1486d8c8e1306a69202527af00c8d8e3 +size 26782 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png new file mode 100644 index 00000000..095337a4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5ad95c958a3261d3da98f890ea6897b452ff1ff2b8ea9940248f05493399a11 +size 27516 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png new file mode 100644 index 00000000..f95dc690 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e67b65be4c37499d5b5a729cd2080483a042974f0e1c503bbab9e4fe23ee129e +size 28136 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png new file mode 100644 index 00000000..9773e4e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d58743b468110eb9f74f94fef5cf9ef927cb7bc42b2573617227bae3ba36da6 +size 26350 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png new file mode 100644 index 00000000..de62bcb6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ccaa97728cd739550412c6f155a208c9665c5656f1b66ae64d0b6978efd0915 +size 28699 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png new file mode 100644 index 00000000..b150344e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263b5e7cf2df1808970e0b4d2a30b35f8860ec4017039a19514811ed41c5b6ca +size 29592 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png new file mode 100644 index 00000000..63e55e5d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8692a111296bb55dd9231dc462c8d251c6aa5c0201011110c0256dde04dbab4 +size 29734 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png new file mode 100644 index 00000000..da552d6f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0fcfd4b4236681ae81520a5b47a0c62c21eda69342a86c295990f4d3b553b6 +size 28241 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..c55e36d8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b74efad8891a0d60a567da00c0ea308a99f22fa1533c4482234c7882fec8acd +size 48329 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..5f591718 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9d070f1d8063725b21885fbb9fedbd94fd4a6a4d63569087eaa2f0f16a75c98 +size 49546 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..68b02b06 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd724a0112357810a8f9e38a8c95526ead36e13df38b986dd3b2599514b50a2c +size 50252 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..35d09c73 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e78e30b6e07f3971c481e9223cbea2cb3fb4ee2fb76aa4c8caa22a549429e2b2 +size 47135 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png new file mode 100644 index 00000000..68a7fd56 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7a8d50203397ec4d7c3747eea5cea9a989a6b442a0de3feeaf442a2c6f8fd5 +size 34701 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png new file mode 100644 index 00000000..03d159df --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75d8064c8bf23494752246d5efd952ec36bd9f7e40f6f800099fedb3fb60eb83 +size 35515 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png new file mode 100644 index 00000000..62dfa72f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a17f15892075b8e43465893037f968651bc01e74acc71a0df21a21e0b25368 +size 35919 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png new file mode 100644 index 00000000..c9263c6c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab0e272cb9b11af768a18558e84300452461760354396d24f6f88bcafc6dc45d +size 34455 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..8655724c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b72da40527736a5b9bd42968c4a245ee0954407be5af9e51474eeaad709ce1f7 +size 59003 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..95613755 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8614a173ec8a2edc96c76e788d809aea84881e13d9ab568c1fc84cb841a8ceda +size 59587 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..fda7c000 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d91fc90a7709bdf286ea45b1da4b04600f6d0a4fad55d1ffe40ae210469a56 +size 60840 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..dd34e59b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c00e8652a47ca1cf59d8e33d491e3720f95fbb3550b4d73ee3ebd7d3c7c92f +size 58223 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png new file mode 100644 index 00000000..aebdf1c5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dc05d16d5077fda6b363a52f3215d404fa07ea75025a0aa78b917de09fed9b2 +size 32670 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png new file mode 100644 index 00000000..3415ab86 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f75e71706840c6138f52a72118077d227eccaaedb8ac67756453c0c41cc4be8 +size 33936 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png new file mode 100644 index 00000000..9fe15de3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c38554939886e02f4c99a466926191ff1f9b6106e6bf0cf3a716af6158fefef6 +size 34259 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png new file mode 100644 index 00000000..9251ed9e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:329a1c98bd7ce5d75c63e8c065424a7ef42a381eb6c620f562e63fb620e7a9d2 +size 32019 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png new file mode 100644 index 00000000..55b2ae05 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_inconsistent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:831303c50c8004c9d998dec6fc84f443a1ea54cba902673aaa079a467bb055ae +size 56314 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png new file mode 100644 index 00000000..f731a0c6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_strong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6a8f37e0f47b9402e84a24424084afacad20262f6d32d3fd06941505db32ea4 +size 57103 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png new file mode 100644 index 00000000..a15ada0a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_verystrong.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6eef1ca41130897a04abb4f1633920dc0b3b8f0da4b14d9ae70e337ab0a025c +size 58265 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png new file mode 100644 index 00000000..5b102f26 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.settings.ui_SettingsSetAppPasswordScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_weak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:058d227c8d5aaf4aba171e2ea14e146ad9a2c0c64adc2c5f9ad4dd23754c654c +size 54796 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..a3a5505d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:135833974d7700b73a44173f1dc64a6b6f5a873fb06b7750b161075a2bd0e95b +size 30907 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..fa0b09c7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dda618405cb7730a61fe72b90c3822d405a9e9d08a8ffc671137d519f17d5e66 +size 35495 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..8f157197 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0910fce533398a02d9a35d6743192ee9f944ec9981b9b6d09370cd286c1eac73 +size 59471 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..598c542c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb851180e02eca909bfbd66a45cf021cdf38931104fcc4e14a2fd41ab305534 +size 30681 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..972da1c0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0be251f24d7fbfd4afa8d9a0655153b5079ea2163d63fbeda722486cf752a29 +size 34961 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..9f9dffe2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90fb94a6cdf44f981c2e9df6c061742bc7e3f74e0e6ffd41bff860d1e2fd8591 +size 58724 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..ed812d55 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:229300d50fc922c33c272c6aa45f09b71826d36eb73408fb109f6e3d777a3ade +size 41409 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..f7532551 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91e093b545c96668619c7a04a6823381691262c12030354e6f81d831b05bc2e4 +size 69669 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..4317ad13 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae7d73b9f36356a8c9c63eefc36d1b3fa6d7c6e5573092255db0670bef512adf +size 40313 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..4cac8134 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingDeviceOnTopScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c1a94ca386cebb10ef669d4b1dab1da53cca4347de4dedff0aa72ad2044c8d6 +size 68397 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..4a0b55ef --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a4ac59c1ce5aa2e8522b99d0466845600d20329ec5c04247cf7c14a697cbd9 +size 43989 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..af1c9282 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68ada16525c6f5450d5cbe21228efe2efcbcc61f8388d550658229512dda549b +size 49108 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..15c46e75 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adde5b5a99df443557a36ae57d23e10e12a8d3bf0817759a52310d51bdc29ea1 +size 80065 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..2af600f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a72d27d5e198e10132764125cc2813a4a3a6e2e2e8658065f2516cb735b84d44 +size 42844 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..4e8ecd94 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f78685a364bfc8ace3eedb11f8ac9875528f8a99fa4c76c7fbd6b2526c735d +size 48243 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..0e7df3a5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:074e2e9756c61b2764451d01d386a1ce34193f637b038316a5768889f016f12f +size 78796 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..17308775 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e346cf4cc81aea6570fb9d0b1577dd9d235652bd9d84a3bdef57aeb2123c571 +size 56821 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..35780e32 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f92e2ac9815b5f840e28bc679af3ef3d6875900f7539b639ba8688f8e2d99cea +size 80348 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..e51a751c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:154eae4fb508d417678a5a7a1cd82ba3748c9c1d9473d84c767034c091e96bdb +size 55697 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..c0e4f9e1 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingFindNfcPositionScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f89fe56c37a5a700508d674417e8263094a1032bf076f5e399de74ea2cbbf90 +size 79084 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png new file mode 100644 index 00000000..6d12cd53 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9dde1d3e2825643da80d638577024ccbe34581657682e70c333c717cc00cd5 +size 40906 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..ba0ef835 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77ac18ea7be7766177ffee2a08830d4be6b0ca7cd93346670bc378e3f64f7507 +size 47080 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..1333c5b5 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bec595b5c14f5e1b279bdbad0089e605dc0b5017aeffc6d9dc46bd41783b6a45 +size 79219 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png new file mode 100644 index 00000000..a1d8f856 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23d653f6c2b5904cdc378c7d2b09019ea9be1a37ffcfa7183878e3b64aafce20 +size 40379 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..4efb7a07 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:825ae07c58065844a527ae3aef0bfb20a5f268932d34db62cc278020fde08877 +size 46253 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..f47aa971 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a4534e4bd6ca9cffa1a3e41525a023b15537a05d2c10fe928a02fc84f80ee5a +size 77896 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png new file mode 100644 index 00000000..15f62426 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630ec4e22b2670c4d4e8cc9fa4a550c84d81ae64bc45db172e5321c820a28b05 +size 54765 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..8776432a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82d9bc76eb1cdffb475ec78f5c0621eb91fb1586fd93d0f33ea07ff3caedb5c8 +size 82088 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png new file mode 100644 index 00000000..b9ddd2c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f220e2c049c080788dcc2a0f7986ce23886358a6bad382466f61745419158294 +size 53366 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png new file mode 100644 index 00000000..2dba639c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingIntroScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b835200e37e796deef7c2eadea0139f22ee224f0a69060faca57e4384436465e +size 80663 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..d76b719a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ba5236d7d0d431458e9148d4c24bf1916b692c9341b0e9c65af709fcd5158a4 +size 60189 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..09dceb19 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbb95776c78614313f093b72d104f2432b862bc0d1936017751376ab4cf00dc6 +size 66970 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..fff652c2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dc3e46a33c452a07c240d1a27cf6515a1deda1dd47cc541ff5f0d97db353b58 +size 85220 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..1afb4e2c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f0ec54bd6d0c51e047fd8d4273dbfd64631f41c520c8ed1047cc4755afdd3f3 +size 59009 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..183bfc5f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b548169e00f0dcbcabf2a8715c6a54ce43c02e699278466e12a0aeae8f2001e2 +size 65058 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..9a8161c4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c0601f657e91bfc68045ce74060dd3f3ee4faeb28b2ef049667490fc1cd6254 +size 83665 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..141ce223 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8a305a2e409184e20dfc80338d0e25bd4db99e77bf7bde933b79f0b0436def0 +size 78531 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..1728aecc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2056a6de88ebbae68ae106c35120bbdd54aae7362d62b7d81862cba1c9ee0a95 +size 92097 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..ceb08339 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82aa2f43665cc27ebc8827550cd524bdd88cd44368c24990cc3ab44c061a4c38 +size 76151 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png new file mode 100644 index 00000000..cdf2715c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.troubleshooting.ui_TroubleShootingNoSuccessScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_troubleshootingnosuccessscreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d88f4c565300d124d5a1072379abb43c527e4a9f54a149afa022fcacc2befe1 +size 90709 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..bc35d1f6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1df5587462fc16b2e8c5769359f866011f3d6a5f66f3d150773f63d24c3ec659 +size 49942 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..3717e006 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8c7b54c49b919d2334ddf1794455b2d0bd9fed4e6f0ab33eae3b54e11eb6d6d +size 53182 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..09ebaede --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d8399352868510d6e189ee3d9a9bb942f76d17c72c8553e2abb9d08e31e09b9 +size 53238 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorpassword.png new file mode 100644 index 00000000..f31d01ca --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffb47983760c405aceef7dc933b608f90fd179b3976364e992fb1490ce253e56 +size 50805 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..2f96646e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b11f5dcaba411ef49159982099d65e96e14b49fdf13b75fe117c21b2a3b8d06 +size 149011 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorboth.png new file mode 100644 index 00000000..0c5f1366 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98a95cc6a93d3ab815ab745b77e487bdc9e2a6c9266ad7b090c84b244b49a1a9 +size 150931 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorpassword.png new file mode 100644 index 00000000..2f96646e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b11f5dcaba411ef49159982099d65e96e14b49fdf13b75fe117c21b2a3b8d06 +size 149011 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..ef5f8b3a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e261475497e4705a14fe789b0e0d17247f143c1a378b77c31ee48c62d78c411 +size 54997 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..867987bb --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c143a96793452f8bb28970bcee88bd831ab090f107a3a7a3bfa6a832436217a +size 55993 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..1c9f4fa6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56b30df1da82a9e71a5d20623c5da3fc3b1667ae626133d8eb02c875665b0439 +size 58260 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorpassword.png new file mode 100644 index 00000000..8b66fc36 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25dd02b391112b52ef33df04b5f593cc249da1d2b32115e0f25f1eacb02ed7d3 +size 57345 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..c0b6aadf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:210bfd84a2ed91049f9d3da46a79b7e36a49a262043b091494659b4396b387e9 +size 150541 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png new file mode 100644 index 00000000..f994778b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e75a425a98e967385ecb9aca671cebb4427cf5e331f1f4f1d0a0706b45014e1a +size 152950 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png new file mode 100644 index 00000000..c0b6aadf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:210bfd84a2ed91049f9d3da46a79b7e36a49a262043b091494659b4396b387e9 +size 150541 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..6c418253 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c1b0064b8eba61c4721b60918694c3bc46fcb19b68d3b88042ee741d199ec91 +size 79583 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..1584d8c3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dacc83fbb38b7866f820da15a4d7e3a3c8fc8314e8de5ec911af9a74b6bec2c3 +size 81501 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..3832ddc4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:820facaa38ee208325ebc5d9bb9622f087cade89b6e1b461df4c5ffd75af523b +size 80551 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png new file mode 100644 index 00000000..af933713 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:105bed43c69b06d652913c287ba202fa515eab6ec77165e2eff4574f4e73c3c0 +size 84640 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..49de8f62 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d9c9387b757fc3faed66f3cb053656e9e659ba429faf8aa9c76236122e727c6 +size 156082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png new file mode 100644 index 00000000..9f6cf9b2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c22f93dc5cc7f7cca60695bd30fd0a9d1e2affc9ea2486bc46adafe06568d41 +size 161272 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png new file mode 100644 index 00000000..49de8f62 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d9c9387b757fc3faed66f3cb053656e9e659ba429faf8aa9c76236122e727c6 +size 156082 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..d213e250 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a83fee2451e6f802e5d1efd70c0b1e235128adc6f728ad9eb2eee1b53e635fb +size 48277 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..0d49a3b7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ad6e975cd808490575037de72ff0e49fe8feee95bc676ce3f370aa8b02b498a +size 51172 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..2667e843 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf68b49d473d7cea9c00a3268ff27219c2eb2eda1edeeac7c468453154a7127 +size 51104 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorpassword.png new file mode 100644 index 00000000..f8d3229a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c9f670097b48bd62042376e563ef0a592f1f20303545caf8615ddb07a0bc9a2 +size 49141 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..74133914 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8ba7d36f97fc11eb47b35be0c871f9d37e94ffdee4d15380bde51fe5eae526 +size 150377 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorboth.png new file mode 100644 index 00000000..6011908d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:804941970a01d6fd758f15243a772003fe431bb804db1b8f1fbebcb635c7927a +size 152261 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorpassword.png new file mode 100644 index 00000000..74133914 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8ba7d36f97fc11eb47b35be0c871f9d37e94ffdee4d15380bde51fe5eae526 +size 150377 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..d2e0bd33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d8523656b1ed7b6e781fe38f38e782bf6f0a74f20d79d0827f564a9068a2f21 +size 52959 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..cb0f1d17 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609a268d9dea5d2c93086c7be05c6c213a05a1ea3780ca136b0d922c1586db91 +size 53975 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..a50ecef4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60381ef285c7abc5faf8dd339a3373c1cea46376479f20ce41291126f35788d7 +size 56661 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png new file mode 100644 index 00000000..73061eb3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:454ebf685fbac8d1c403403c6c03de38ea988cfb242a1b3fe6feb8fcdb59d3b8 +size 55327 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..c021fcbf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0745ad80196f4d483f89be3fffcb091c32022041757b228abb650aadfa79f21a +size 151784 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png new file mode 100644 index 00000000..e24f015e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9689f664df3e58ac6b7876abd7749364d448a724614ee1f9575c1af1b1542f7e +size 154081 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png new file mode 100644 index 00000000..c021fcbf --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0745ad80196f4d483f89be3fffcb091c32022041757b228abb650aadfa79f21a +size 151784 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..0a810a27 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95cb5a7871d8bbc6fda4eada47d936ab1611a93d958711f5fcc7f2ab28eee2cd +size 77264 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..47ff759b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65afdea53eee9e3271e12f959390f60e457e718290fe5442e872fbb5b4109149 +size 79115 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..71c77cfa --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2964d442dea068184029e9facaa5b967fa5ee678c7828bfe6262385969cdeb7 +size 78328 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png new file mode 100644 index 00000000..0e9b259b --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58fda212e86329552ae411513ddfe456c1c8f409817f31ad80f1f4299531cf77 +size 81565 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..1c730a2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e12304fb76a37c341740329d8dbbf130cf02ce3bfe85d8993f233c5bd4da938a +size 156990 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png new file mode 100644 index 00000000..18750542 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebafef7addc336e5be83c06a2041cf336fbc2c49098a6dcbe46526e1f9372e28 +size 161973 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png new file mode 100644 index 00000000..1c730a2d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e12304fb76a37c341740329d8dbbf130cf02ce3bfe85d8993f233c5bd4da938a +size 156990 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..eb219f91 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58fad6e6785f636acc6e9d922b885ea7c44913875497f2c589d09312b6ed1f8d +size 62747 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..965f3bac --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f24b2d40e2415920c1a8a1c82a2924c1d6e76aa69296f393dc58aad4730bf8f +size 64245 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..85f89790 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bca65f09fd7cc23dad2c0502abbf447a234f68e4490359167247d5561297bfe1 +size 67483 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorpassword.png new file mode 100644 index 00000000..d9c538e4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:964fd063fecff583aac402bfacb3a3c6376abf55b22b4ef9d1894de1cb4775ce +size 65506 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..44ae23f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d9a71c4b9f087728d318d880ad27830786a53c58ba24401faa592744595a4d +size 159215 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png new file mode 100644 index 00000000..2027f4a6 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1804ab17247c1012f6fafe38033ce780c734e75d8a22d2358a9def954fb9c30a +size 162691 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png new file mode 100644 index 00000000..44ae23f8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d9a71c4b9f087728d318d880ad27830786a53c58ba24401faa592744595a4d +size 159215 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..12d48974 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:deae8cace2a3aad8360da1d63f2450e19c856f8fa3c744980acdfc7695957223 +size 72006 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..badb1635 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0647f65027acbd8254d3aec2db88db910530b3f121ac7cd7139b9f71ca5e7d45 +size 74663 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..d8746ae2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:146d6ba646d9f67f48e1aa60b439c6f962b9e19bb07e16dcb83c13cf5057b99d +size 74743 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png new file mode 100644 index 00000000..d127ef7c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf402730cf61e9d94c1538c21e4dcd759b8176c88ad5fc0f978b46da5bc63540 +size 71153 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..2a0219e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c2ddf6743fc9e124f1ff5acfa4f1014478ed6ae55bf6c1f47ba1942ac5e2e53 +size 167514 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png new file mode 100644 index 00000000..b2fcb531 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abc65729212dc67cd966fd76ec41f792e6e04fd1e1679df2f9265f0274dbb06e +size 173351 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png new file mode 100644 index 00000000..2a0219e0 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c2ddf6743fc9e124f1ff5acfa4f1014478ed6ae55bf6c1f47ba1942ac5e2e53 +size 167514 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..6cc1082a --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb1c814acf921f6d77208fb3d9fbc3e8b6002412f879edbff6c7bfd5a1cd7888 +size 60165 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..79bf2940 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da5f0af60c433270ba8dcd18db5f4c815b1d95da6d9e70aa7d345359e516e7b +size 61841 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..c13b1b6d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53e56310f28a0da1d90a6a1fbae0e40431326ade675d402e8c1f7c1937e814f0 +size 64889 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png new file mode 100644 index 00000000..900d466c --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65cbac6b7ae9c9897c4b491167dbe8a3e8f5dbc79358c67c5cbe8a4ee3c84506 +size 62955 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..b7a5c24f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:323f00e01896ecba6dc0106d8af66b0f6d6ece547233b61bd6937356f1d8b53b +size 160172 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png new file mode 100644 index 00000000..5e6ce72d --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:057abfebdfca44c73d9f1c34198a597c02e373a5e7047fc0ef55f31d15ca8c90 +size 163472 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png new file mode 100644 index 00000000..b7a5c24f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:323f00e01896ecba6dc0106d8af66b0f6d6ece547233b61bd6937356f1d8b53b +size 160172 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png new file mode 100644 index 00000000..d42cab80 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8fe51faaf4722e50bd001e364c4e895ff5e0b7aaf58898644994b5a895ff30 +size 69542 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png new file mode 100644 index 00000000..a79bafd8 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbiometrylockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3c62d7fd253bd8588dccd7f6c499c0cf40c5f28d6ca0b9f7f0f6d212091905e +size 71470 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png new file mode 100644 index 00000000..8837ea45 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorbothlockoutpermanent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58b292437ea6db7d87dae1e131fe0133864210c98433fbb3e2877de54f922590 +size 71560 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png new file mode 100644 index 00000000..7eaf192f --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistateerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4026a151b06a9acdf30bc8e4aa6f49af37750671368b9d76af88799355c5cc46 +size 69316 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png new file mode 100644 index 00000000..a1d99bce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorbiometry.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5af46742a265fa9d5158beecba61b46b3dee4ce850ac5a53a6728a9765319b5e +size 168388 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png new file mode 100644 index 00000000..be3f8d33 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorboth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df289ba3124533f5885897ccc6278d0b77adc3b51c7e2cdf767fae0ff5ac75a9 +size 174139 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png new file mode 100644 index 00000000..a1d99bce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.userauthentication_UserAuthenticationScreenTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_uistatenoerrorpassword.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5af46742a265fa9d5158beecba61b46b3dee4ce850ac5a53a6728a9765319b5e +size 168388 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nointernetbanner.png new file mode 100644 index 00000000..1eafcbe2 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_ENGLISH]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:664563fc811da5e49c808e4995db505f68cc9a6572c777875d737e9704013213 +size 6373 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nointernetbanner.png new file mode 100644 index 00000000..17d5f1ce --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7546efa233a8fd1e5bd2c5db288f53fed1350436cb0a6cc4f0be1a36d456b209 +size 6734 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png new file mode 100644 index 00000000..0c541912 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff2ccdd00b3ef6dc26334fd267a61e3375e2967ad76272d58300d25b352511e5 +size 8973 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nointernetbanner.png new file mode 100644 index 00000000..f0362513 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_ENGLISH]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8768523b057b70905e3959a9dad20e1266b194a2f653e44753766b48850266e +size 6438 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nointernetbanner.png new file mode 100644 index 00000000..cb3ee3b4 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96cdc418d80f55a64eee27982a7f67b0bbf65bafc2003ec51f843ac0905c0b5 +size 6790 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png new file mode 100644 index 00000000..dc402bf3 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[LARGE_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caf5ced26d722040f7404d228e1c18d8aabbd532aea79634e561f3fcffbb3e96 +size 9153 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nointernetbanner.png new file mode 100644 index 00000000..ecbcbe9e --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87f850b062afd5a722a010ea8bef6409e53a903de42c42e330c7cb360797fad8 +size 7252 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png new file mode 100644 index 00000000..221794ee --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_DARK_GERMAN_BIG_FONT]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a69b8c8743601defb9ea13cec4d2d13297767b6015e8a8609830553d2ab1c46e +size 9914 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nointernetbanner.png new file mode 100644 index 00000000..d5e80be7 --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3572e813e73c8e9b4f467dd0e51ea3e8e650f2aa3eb3ba3a8080a0256cfdd918 +size 7319 diff --git a/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png new file mode 100644 index 00000000..06406bdc --- /dev/null +++ b/app/features/src/test/snapshots/images/de.gematik.ti.erp.app.utils.banner.ui_NoInternetBannerTest_screenShotTest[SMALL_SCREEN_LIGHT_GERMAN_BIG_FONT]_nointernetbanner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77899c1879e6e554e275920d4df93deefd52daa633a829acfda046c4f99a16bb +size 10087 diff --git a/app/shared-test/build.gradle.kts b/app/shared-test/build.gradle.kts deleted file mode 100644 index b6312df9..00000000 --- a/app/shared-test/build.gradle.kts +++ /dev/null @@ -1,113 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import org.owasp.dependencycheck.reporting.ReportGenerator.Format - -plugins { - id("com.android.library") - kotlin("android") - kotlin("plugin.serialization") - id("org.jetbrains.compose") - id("io.realm.kotlin") - id("kotlin-parcelize") - id("org.owasp.dependencycheck") - id("com.jaredsburrows.license") - id("de.gematik.ti.erp.dependencies") - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") - id("de.gematik.ti.erp.technical-requirements") -} - -licenseReport { - generateCsvReport = false - generateHtmlReport = false - generateJsonReport = true - copyJsonReportToAssets = true -} - -android { - namespace = "de.gematik.ti.erp.app.sharedtest" - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - dependencyCheck { - analyzers.assemblyEnabled = false - suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" - formats = listOf(Format.HTML, Format.XML) - scanConfigurations = configurations.filter { - it.name.startsWith("api") || - it.name.startsWith("implementation") || - it.name.startsWith("kapt") - }.map { it.name } - } - buildTypes { - create("minifiedDebug") - } -} - -dependencies { - implementation(project(":common")) - implementation(project(mapOf("path" to ":app:features"))) - testImplementation(project(":common")) - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) - testImplementation(kotlin("test")) - - inject { - androidX { - implementation(legacySupport) - implementation(appcompat) - implementation(coreKtx) - implementation(datastorePreferences) - implementation(security) - implementation(biometric) - implementation(webkit) - - implementation(lifecycleViewmodel) - implementation(lifecycleComposeRuntime) - implementation(lifecycleProcess) - implementation(composeNavigation) - implementation(composeActivity) - implementation(composePaging) - implementation(camerax2) - implementation(cameraxLifecycle) - implementation(cameraxView) - } - logging { - implementation(napier) - } - androidXTest { - testImplementation(archCore) - implementation(core) - implementation(rules) - implementation(junitExt) - implementation(runner) - androidTestUtil(orchestrator) - androidTestUtil(services) - implementation(navigation) - implementation(espresso) - implementation(espressoIntents) - } - coroutinesTest { - testImplementation(coroutinesTest) - } - composeTest { - implementation(ui) - debugImplementation(uiManifest) - implementation(junit4) - } - test { - testImplementation(junit4) - testImplementation(snakeyaml) - testImplementation(json) - testImplementation(mockk) - androidTestImplementation(mockkAndroid) - } - } -} - -secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) "ci-overrides.properties" else "gradle.properties" -} diff --git a/app/shared-test/src/main/AndroidManifest.xml b/app/shared-test/src/main/AndroidManifest.xml deleted file mode 100644 index 10728cc7..00000000 --- a/app/shared-test/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt deleted file mode 100644 index e9011e1a..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.sharedtest.testresources.actions - -import androidx.compose.ui.test.junit4.ComposeTestRule -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig -import de.gematik.ti.erp.app.sharedtest.testresources.screens.MainScreen -import de.gematik.ti.erp.app.sharedtest.testresources.screens.OnboardingScreen -import de.gematik.ti.erp.app.sharedtest.testresources.screens.PharmacySearchScreen - - -// ZoTI - Testapotheken Pharmacies Reference: https://wiki.gematik.de/display/DEV/ZoTI+-+Testapotheken -class PharmacyScreenAction(private val composeRule: ComposeTestRule) { - - private val onboardingScreen by lazy { OnboardingScreen(composeRule) } - private val mainScreen by lazy { MainScreen(composeRule) } - private val pharmacySearchScreen by lazy { PharmacySearchScreen(composeRule) } - - /** - * For Zoti Pharmacy -> 2 - * PickupService -> Enabled - * */ - fun pickupServiceSuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti02) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy -> 1,5 - * PickupService -> Disabled - * */ - fun pickupServiceFailTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti01) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti05) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - /** - * For Zoti Pharmacy -> 3 - * CourierDelivery -> Enabled - * */ - fun courierDeliverySuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti03) - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy -> 6 - * CourierDelivery > Disabled - * */ - fun courierDeliveryFailTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti06) - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - /** - * For Zoti Pharmacy -> 7 - * MailDelivery > Disabled - * */ - fun mailDeliveryFailTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti07) - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - /** - * For Zoti Pharmacy -> 12 - * PickupService -> Disabled - * MailDelivery > Disabled - * CourierDelivery > Disabled - * */ - fun pickupServiceMailDeliveryCourierDeliveryFailTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti12) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - pharmacySearchScreen.awaitOrderOptionsEnabled() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - pharmacySearchScreen.awaitOrderOptionsEnabled() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - /** - * For Zoti Pharmacy -> 14, 15 - * PickupService -> Enabled - * MailDelivery > Enabled - * */ - fun pickupServiceMailDeliverySuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti14) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti15) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy ->4, 16 - * MailDelivery > Enabled - * */ - fun mailDeliverySuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti04) - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti16) - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy -> 13, 17 - * PickupService -> Enabled - * CourierDelivery > Enabled - * */ - fun pickupServiceCourierSuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti13) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti17) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - /** - * For Zoti Pharmacy -> 8, 9, 10, 11 & 18 - * PickupService -> Enabled - * MailDelivery > Enabled - * CourierDelivery > Enabled - * */ - fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti08) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti09) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti10) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti11) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - - pharmacySearchScreen.dismissOrderOptionsBottomSheet() - - goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti18) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy -> 19 - * MailDelivery > Enabled - * CourierDelivery > Enabled - * */ - fun mailDeliveryCourierDeliverySuccessTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti19) - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - pharmacySearchScreen.userClicksOnOrderByCourierDelivery() - pharmacySearchScreen.checkAndClickNoPrescriptionDialog() - } - - /** - * For Zoti Pharmacy -> 20 - * PickupService -> Disabled - * MailDelivery > Disabled - * */ - fun pickupServiceMailDeliveryFailTest() { - navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti20) - pharmacySearchScreen.userClicksOnOrderByPickUp() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - pharmacySearchScreen.awaitOrderOptionsEnabled() - pharmacySearchScreen.userClicksOnOrderByMailDelivery() - pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() - } - - private fun navigateToPharmacyOrderOptions(pharmacyName: String) { - onboardingScreen.tapSkipOnboardingButton() - mainScreen.openPharmaciesFromBottomBarFromStart() - pharmacySearchScreen.openOrderOptionsAndClickSearchButton() - pharmacySearchScreen.searchPharmacyByName(pharmacyName) - } - - private fun goToPharmacyOrderOptionsAfterBottomSheetDismissed(pharmacyName: String) { - pharmacySearchScreen.userClicksOnPharmacyFromListByName(pharmacyName) - pharmacySearchScreen.userSeesPharmacyOrderOptions() - pharmacySearchScreen.awaitOrderOptionsEnabled() - } -} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt deleted file mode 100644 index 5ecd3ec6..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("ktlint:max-line-length") - -package de.gematik.ti.erp.app.sharedtest.testresources.config - -object TestConfig { - const val WeakPassword = "TrustNo1" - const val StrongPassword = "Jaja Ding Dong!" - const val DefaultProfileName = "Rainer Reizdarm" - const val DefaultEGKCAN = "123123" - const val DefaultEGKPassword = "123456" - const val WaitTimeout1MilliSec = 100L - const val WaitTimeout1Sec = 1_000L - const val ScreenChangeTimeout = 3_000L - const val WaitTimeout5Sec = 5_000L - const val LoadPrescriptionsTimeout = 20_000L - - const val AppDefaultVirtualEgkKvnr = "X764228532" - const val PharmacyName = "Apotheke am Flughafen - E2E-Test" - const val PharmacyTelematikId = "3-SMC-B-Testkarte-883110000116873" - - const val PharmacyZoti = "ZoTI" - const val PharmacyZoti01 = "ZoTI_01_TEST-ONLY" - const val PharmacyZoti02 = "ZoTI_02_TEST-ONLY" - const val PharmacyZoti03 = "ZoTI_03_TEST-ONLY" - const val PharmacyZoti04 = "ZoTI_04_TEST-ONLY" - const val PharmacyZoti05 = "ZoTI_05_TEST-ONLY" - const val PharmacyZoti06 = "ZoTI_06_TEST-ONLY" - const val PharmacyZoti07 = "ZoTI_07_TEST-ONLY" - const val PharmacyZoti08 = "ZoTI_08_TEST-ONLY" - const val PharmacyZoti09 = "ZoTI_09_TEST-ONLY" - const val PharmacyZoti10 = "ZoTI_10_TEST-ONLY" - const val PharmacyZoti11 = "ZoTI_11_TEST-ONLY" - const val PharmacyZoti12 = "ZoTI_12_TEST-ONLY" - const val PharmacyZoti13 = "ZoTI_13_TEST-ONLY" - const val PharmacyZoti14 = "ZoTI_14_TEST-ONLY" - const val PharmacyZoti15 = "ZoTI_15_TEST-ONLY" - const val PharmacyZoti16 = "ZoTI_16_TEST-ONLY" - const val PharmacyZoti17 = "ZoTI_17_TEST-ONLY" - const val PharmacyZoti18 = "ZoTI_18_TEST-ONLY" - const val PharmacyZoti19 = "ZoTI_19_TEST-ONLY" - const val PharmacyZoti20 = "ZoTI_20_TEST-ONLY" - - object FD { - const val DefaultServer = "https://erpps-test.dev.gematik.solutions" - const val DefaultDoctor = "9a15b6f9f4b8f2e9df1db745a4091bbd" - const val DefaultPharmacy = "886c6eda7dd5a1c6b1d112907f544d3" - } -} - -interface VirtualEgk { - val certificate: String - val privateKey: String - val kvnr: String - val name: String -} - -object VirtualEgk1 : VirtualEgk { - @Suppress("MaxLineLength") - override val certificate = - "MIIDXTCCAwSgAwIBAgIHAs9vZEwB8jAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAxMjQwMDAwMDBaFw0yNzAxMjMyMzU5NTlaMIHgMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDU5Mjk3MTEUMBIGA1UEBAwLVsOzcm13aW5rZWwxHDAaBgNVBCoME1hlbmlhIFZlcmEgQWRlbGhlaWQxEjAQBgNVBAwMCVByb2YuIERyLjE1MDMGA1UEAwwsUHJvZi4gRHIuIFhlbmlhIFZlcmEgQS4gVsOzcm13aW5rZWxURVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAEczsMfajcnKpGYyNeXUhODjyrX4z4j9Qzio/Ulq5COPVySk0CxYBDj+1VEd5FalhEJXC9HjVRCflQx+RkEQFbvqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBTCDfBZ8X30CZnFk7E2x8+lMM5uODA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIDDAXcyOKDYOZpoH0iYijr1yisyxHeT3ch6XZlFNXPrKAiAHepW4TOQAoqyoGG9Pgly0TO2tTB7WLKEc7B3F6lNhpA==" - override val privateKey = "AJzshqeIuhwReqZpWbqY0PnRjTdTRzk4Zj9GpSxcUukA" - override val kvnr = "X110592971" - override val name = "Vórmwinkel Xenia Vera Adelheid" -} - -object VirtualEgkWithPrescription : VirtualEgk { - @Suppress("MaxLineLength") - override val certificate = - "MIIDLTCCAtSgAwIBAgIHAZ/zfVKUfTAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAyMDQwMDAwMDBaFw0yNzAyMDMyMzU5NTlaMIGwMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDUzNTU0MTEOMAwGA1UEBAwFS2zDtm4xDTALBgNVBCoMBEx1Y2ExDDAKBgNVBAwMA0RyLjEgMB4GA1UEAwwXRHIuIEx1Y2EgS2zDtm5URVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAETn/MKYxsnBH9khicaXG3mFc5v4RoL0ILuJ3TreTsiFsv91OA6Yj/O4EAxm6dCpPtGgWRyVUYbOgDkaDurSUPpqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBRhIkfxtBhE+Z3fcu+OWu/3gnnYqjA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIGHDnSVg2A9NmFPhtzo4dL3CVbN94k3NrYhXLOZoCUFXAiBlE6TfW6uL91jhv8JuupHhr7X6B9YcbVizWoMxo1grFA==" - override val privateKey = "cv2z1KGMJi+M5foz3GCz0bi5pSdBIjVTqw2cUuIsJcY=" - override val kvnr = "X110535541" - override val name = "Klön Luca" -} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt deleted file mode 100644 index 14cbb7a9..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.sharedtest.testresources.screens - -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeDown -import androidx.compose.ui.test.swipeUp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout5Sec -import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay - -@Suppress("TooManyFunctions") -class MainScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesMainScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreen) - } - - fun userSeesBottomSheet(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { - composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreenBottomSheet.Modal) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.Modal).performTouchInput { swipeUp() } - } - - fun checkProfileHasState(profileName: String, profileState: String) { - onNodeWithText(profileName, substring = true) - .assertIsDisplayed() - onNodeWithText(profileState, substring = true) - .assertIsDisplayed() - } - - fun refreshMainScreenBySwipe() { - onNodeWithTag(TestTag.Main.MainScreen) - .assertIsDisplayed() - .performTouchInput { swipeDown() } - } - - fun tapLoginButton() { - onNodeWithTag(TestTag.Main.LoginButton) - .assertIsDisplayed() - .performClick() - } - - fun tapConnectLater() { - userSeesBottomSheet(WaitTimeout5Sec) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ConnectLaterButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapTooltips() { - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - onRoot().performClick() - } - - fun tapSettingsButton() { - onNodeWithTag(TestTag.BottomNavigation.SettingsButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksBottomBarPrescriptions() { - onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksBottomBarPharmacy() { - onNodeWithTag(TestTag.BottomNavigation.PharmaciesButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun userClicksPharmacySearchBar() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - TestConfig.ScreenChangeTimeout - } - - fun userClicksBottomBarOrders() { - onNodeWithTag(TestTag.BottomNavigation.OrdersButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapAddProfileButton() { - onNodeWithTag(TestTag.Main.AddProfileButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun enterProfileName(name: String) { - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) - .assertIsDisplayed() - .performClick() - .performTextInput(name) - } - - fun tapNewProfileConfirmButton() { - userSeesBottomSheet(WaitTimeout5Sec) - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } - - fun tapCancelAddProfileButton() { - userSeesBottomSheet() - onNodeWithTag(TestTag.Main.LoginButton) // BottomSheet has no CancelButton - .performClick() - } - - fun assertConfirmationCanNotBeClicked() { - onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) - .assertIsNotEnabled() - } - - /** - * cancel user login - * click-though all tool-tips - * click on pharmacies bottom button - */ - fun openPharmaciesFromBottomBarFromStart() { - tapConnectLater() - tapTooltips() - userClicksBottomBarPharmacy() - } -} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt deleted file mode 100644 index dd044683..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.sharedtest.testresources.screens - -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.assertIsOff -import androidx.compose.ui.test.assertIsOn -import androidx.compose.ui.test.assertIsToggleable -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeLeft -import androidx.compose.ui.test.swipeRight -import androidx.compose.ui.test.swipeUp -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout5Sec -import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay - -@Suppress("TooManyFunctions") -class OnboardingScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun checkTutorialIsNotPresent() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertDoesNotExist() - } - - fun waitForSecondOnboardingPage() { - composeRule.awaitDisplay(WaitTimeout5Sec, TestTag.Onboarding.DataTermsScreen) - } - - fun waitForAnalyticsPage() { - composeRule.awaitDisplay(WaitTimeout5Sec, TestTag.Onboarding.Analytics.ScreenContent) - } - - fun tapContinueButton() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .performClick() - } - - fun switchToPasswordMode() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordTab) - .assertIsDisplayed() - .performClick() - } - - fun enterPasswordA(password: String) { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldA) - .assertIsDisplayed() - .performClick() - .assertIsFocused() - .performTextInput(password) - } - - fun enterPasswordB(password: String) { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldB) - .assertIsDisplayed() - .performClick() - .assertIsFocused() - .performTextInput(password) - } - - fun tapDataTermsSwitch() { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .performClick() - } - - fun closeDataProtection() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun checkDataProtectionIsDisplayed() { - onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) - .assertIsDisplayed() - } - - fun openDataProtection() { - onNodeWithTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton) - .assertIsDisplayed() - .performClick() - } - - fun checkDataProtectionAreNotDisplayed() { - onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) - .assertDoesNotExist() - } - - fun closeTermsOfUse() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun openTermsOfUse() { - onNodeWithTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton) - .assertIsDisplayed() - .performClick() - } - - fun checkTermsOfUseAreDisplayed() { - onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) - .assertIsDisplayed() - } - - fun checkTermsOfUseAreNotDisplayed() { - onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) - .assertDoesNotExist() - } - - fun checkNoPasswordErrorMessagePresent() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) - .assertIsDisplayed() - .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "sufficient")) - } - - fun checkPasswordErrorMessagePresent() { - onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) - .assertIsDisplayed() - .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "insufficient")) - } - - fun checkContinueTutorialButtonIsEnabled() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsEnabled() - } - - fun checkContinueTutorialButtonIsDeactivated() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsNotEnabled() - } - - fun checkDataTermsSwitchDeactivated() { - onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOff() - } - - fun checkWelcomePageIsPresent() { - onNodeWithTag(TestTag.Onboarding.WelcomeScreen) - .assertExists() - } - - fun checkCredentialsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.CredentialsScreen) - .assertExists() - } - - fun checkAnalyticsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.AnalyticsScreen) - .assertExists() - } - - fun checkDataTermsPageIsPresent() { - onNodeWithTag(TestTag.Onboarding.DataTermsScreen) - .assertExists() - } - - fun swipeToNextTutorialStep() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertIsDisplayed() - .performTouchInput { swipeLeft() } - } - - fun swipeToPreviousTutorialStep() { - onNodeWithTag(TestTag.Onboarding.Pager) - .assertIsDisplayed() - .performTouchInput { swipeRight() } - } - - fun checkContinueTutorialButtonIsDisabled() { - onNodeWithTag(TestTag.Onboarding.NextButton) - .assertIsDisplayed() - .assertIsNotEnabled() - } - - fun tapSkipOnboardingButton() { - onNodeWithTag(TestTag.Onboarding.SkipOnboardingButton) - .assertIsDisplayed() - .performClick() - } - - fun checkAnalyticsSwitchIsDeactivated() { - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .performScrollTo() - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOff() - } - - fun checkAnalyticsSwitchIsActivated() { - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .performScrollTo() - .assertIsDisplayed() - .assertIsToggleable() - .assertIsOn() - } - - fun toggleAnalyticsSwitch() { - onNodeWithTag(TestTag.Onboarding.ScreenContent) - .performTouchInput { - swipeUp() - } - - onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) - .assertIsDisplayed() - .assertIsToggleable() - .performClick() - } - - fun tapAcceptAnalyticsButton() { - onNodeWithTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) - .assertIsDisplayed() - .assertHasClickAction() - .performClick() - } -} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt deleted file mode 100644 index b71346e1..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.sharedtest.testresources.screens - -import android.view.KeyEvent -import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsEnabled -import androidx.compose.ui.test.assertIsFocused -import androidx.compose.ui.test.assertIsNotSelected -import androidx.compose.ui.test.assertIsSelected -import androidx.compose.ui.test.filterToOne -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasAnyDescendant -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.isSelected -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToKey -import androidx.compose.ui.test.performScrollToNode -import androidx.compose.ui.test.performTextInput -import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions.pressKey -import androidx.test.espresso.matcher.ViewMatchers -import de.gematik.ti.erp.app.PrescriptionIds -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.LoadPrescriptionsTimeout -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout1Sec -import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay -import de.gematik.ti.erp.app.sharedtest.testresources.utils.hasPharmacyId -import de.gematik.ti.erp.app.sharedtest.testresources.utils.hasPrescriptionId -import de.gematik.ti.erp.app.sharedtest.testresources.utils.sleep - -@Suppress("TooManyFunctions") -class PharmacySearchScreen(private val composeRule: ComposeTestRule) : - SemanticsNodeInteractionsProvider by composeRule { - - fun userSeesPharmacySearchResultScreen() { - onNodeWithTag(TestTag.PharmacySearch.ResultScreen) - .assertIsDisplayed() - } - - fun awaitSearchResults() { - composeRule.awaitDisplay(LoadPrescriptionsTimeout) { - onNodeWithTag(TestTag.PharmacySearch.ResultContent) - .assertIsDisplayed() - .assert(hasAnyChild(hasTestTag(TestTag.PharmacySearch.PharmacyListEntry))) - } - composeRule.sleep(WaitTimeout1Sec) - } - - fun userClicksOnTestPharmacy() { - onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) - .assertIsDisplayed() - .performScrollToNode(hasPharmacyId(TestConfig.PharmacyTelematikId)) - .onChildren() - .filterToOne(hasPharmacyId(TestConfig.PharmacyTelematikId)) - .performClick() - } - - fun userClicksOnPharmacyFromListByName(name: String) { - composeRule.sleep(WaitTimeout1Sec) - onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) - .performScrollToNode(hasAnyDescendant(hasText(name))) - .onChildren() - .filterToOne(hasAnyDescendant(hasText(name))) - .performClick() - } - - fun userSeesPharmacyOrderOptions() { - composeRule.sleep(WaitTimeout1Sec) - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.Content, useUnmergedTree = true) - .assertExists() - } - - fun awaitOrderOptionsEnabled() { - composeRule.sleep(WaitTimeout1Sec) - } - - fun dismissOrderOptionsBottomSheet() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchField) - .assertIsDisplayed() - .performClick() - } - - fun userClicksOnOrderByCourierDelivery() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun userClicksOnOrderByPickUp() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun checkToastMessageWhenOrderOptionClicked() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.ComposeToast, useUnmergedTree = true) - .assertExists() - } - - fun checkAndClickNoPrescriptionDialog() { - onNodeWithTag(TestTag.AlertDialog.ConfirmButton).assertIsDisplayed().performClick() - } - - fun userClicksOnOrderByMailDelivery() { - onNodeWithTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun userSeesPharmacyOrderSummaryScreen() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.Screen) - .assertIsDisplayed() - } - - fun userSeesSendOrderButtonEnabled() { - // asynchronous process enabling the button - composeRule.awaitDisplay(WaitTimeout1Sec) { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) - .assertIsDisplayed() - .assertIsEnabled() - } - } - - fun userClicksPrescriptionSelection() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton) - .assertIsDisplayed() - .performClick() - } - - fun userSeesPrescriptionSelectionScreen() { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen) - .assertIsDisplayed() - } - - fun userDeselectsAllPrescriptions() { - val prescriptionIds = onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .fetchSemanticsNode() - .config[PrescriptionIds]!! - - prescriptionIds.forEach { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .performScrollToKey("prescription-$it") - .onChildren() - .filterToOne(hasPrescriptionId(it).and(isSelected())) - .performClick() - } - } - - fun userSelectsPrescription(prescription: PrescriptionUseCaseData.Prescription) { - onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) - .assertIsDisplayed() - .performScrollToKey("prescription-${prescription.taskId}") - .onChildren() - .filterToOne(hasPrescriptionId(prescription.taskId)) - .assertIsNotSelected() - .assertIsDisplayed() - .performClick() - .assertIsSelected() - } - - fun userClicksBack() { - onNodeWithTag(TestTag.TopNavigation.BackButton) - .assertIsDisplayed() - .performClick() - } - - fun userClicksSendOrderButton() { - onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) - .assertIsDisplayed() - .assertIsEnabled() - .performClick() - } - - fun userSearchesForTestPharmacy() { - onNodeWithTag(TestTag.PharmacySearch.TextSearchField) - .performClick() - .assertIsFocused() - .performTextInput(TestConfig.PharmacyZoti) - - Espresso.onView(ViewMatchers.isRoot()).perform(pressKey(KeyEvent.KEYCODE_ENTER)) - } - - fun openOrderOptionsAndClickSearchButton() { - onNodeWithTag(TestTag.PharmacySearch.OverviewScreen, useUnmergedTree = true) - .assertIsDisplayed() - onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) - .performClick() - } - - fun searchPharmacyByName(name: String) { - userSearchesForTestPharmacy() - awaitSearchResults() - userClicksOnPharmacyFromListByName(name) - userSeesPharmacyOrderOptions() - awaitOrderOptionsEnabled() - } -} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt deleted file mode 100644 index 225fd0c4..00000000 --- a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.sharedtest.testresources.utils - -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.semantics.getOrNull -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.SemanticsNodeInteractionCollection -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.printToString -import androidx.test.platform.app.InstrumentationRegistry -import de.gematik.ti.erp.app.InsuranceState -import de.gematik.ti.erp.app.MedicationCategory -import de.gematik.ti.erp.app.PharmacyId -import de.gematik.ti.erp.app.PrescriptionId -import de.gematik.ti.erp.app.SubstitutionAllowed -import de.gematik.ti.erp.app.SupplyForm -import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout1MilliSec - -fun ComposeTestRule.awaitDisplay(timeout: Long, vararg tags: String): String { - val t0 = System.currentTimeMillis() - do { - tags.forEach { tag -> - try { - onNodeWithTag(tag).assertIsDisplayed() - return tag - } catch (_: AssertionError) { - } - } - mainClock.advanceTimeBy(WaitTimeout1MilliSec) - Thread.sleep(WaitTimeout1MilliSec) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.awaitDisplay(timeout: Long, node: () -> SemanticsNodeInteraction) { - val t0 = System.currentTimeMillis() - do { - try { - node().assertIsDisplayed() - return - } catch (_: AssertionError) { - } - mainClock.advanceTimeBy(WaitTimeout1MilliSec) - Thread.sleep(WaitTimeout1MilliSec) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.await(timeout: Long, node: () -> Unit) { - val t0 = System.currentTimeMillis() - do { - try { - node() - return - } catch (_: AssertionError) { - } - mainClock.advanceTimeBy(WaitTimeout1MilliSec) - Thread.sleep(WaitTimeout1MilliSec) - } while (System.currentTimeMillis() - t0 < timeout) - throw AssertionError( - "Node was not displayed after $timeout milliseconds. Root node was:\n${ - onRoot().printToString(Int.MAX_VALUE) - }" - ) -} - -fun ComposeTestRule.sleep(timeout: Long) { - val t0 = System.currentTimeMillis() - do { - mainClock.advanceTimeBy(WaitTimeout1MilliSec) - Thread.sleep(WaitTimeout1MilliSec) - } while (System.currentTimeMillis() - t0 < timeout) -} - -fun SemanticsNodeInteraction.assertHasText(includeEditableText: Boolean = true) = - assert(hasText(includeEditableText)) - -fun hasText( - includeEditableText: Boolean = true -): SemanticsMatcher { - val propertyName = if (includeEditableText) { - "${SemanticsProperties.Text.name} + ${SemanticsProperties.EditableText.name}" - } else { - SemanticsProperties.Text.name - } - return SemanticsMatcher( - propertyName - ) { node -> - val actual = mutableListOf() - if (includeEditableText) { - node.config.getOrNull(SemanticsProperties.EditableText) - ?.let { actual.add(it.text) } - } - node.config.getOrNull(SemanticsProperties.Text) - ?.let { actual.addAll(it.map { anStr -> anStr.text }) } - actual.all { it.isNotBlank() } - } -} - -fun SemanticsNodeInteractionCollection.assertNone( - matcher: SemanticsMatcher -): SemanticsNodeInteractionCollection = - filter(matcher) - .assertCountEquals(0) - -fun hasPrescriptionId(id: String): SemanticsMatcher = - SemanticsMatcher.expectValue(PrescriptionId, id) - -fun hasPharmacyId(id: String): SemanticsMatcher = - SemanticsMatcher.expectValue(PharmacyId, id) - -fun hasInsuranceState(state: String?): SemanticsMatcher = - SemanticsMatcher.expectValue(InsuranceState, state) - -fun hasSubstitutionAllowed(allowed: Boolean): SemanticsMatcher = - SemanticsMatcher.expectValue(SubstitutionAllowed, allowed) - -fun hasSupplyForm(form: String): SemanticsMatcher = - SemanticsMatcher.expectValue(SupplyForm, form) - -fun hasMedicationCategory(form: String): SemanticsMatcher = - SemanticsMatcher.expectValue(MedicationCategory, form) - -fun execShellCmd(cmd: String) { - InstrumentationRegistry.getInstrumentation().uiAutomation - .executeShellCommand(cmd) -} diff --git a/app/shared-test/.gitignore b/app/test-tags/.gitignore similarity index 100% rename from app/shared-test/.gitignore rename to app/test-tags/.gitignore diff --git a/app/test-tags/build.gradle.kts b/app/test-tags/build.gradle.kts new file mode 100644 index 00000000..d3298485 --- /dev/null +++ b/app/test-tags/build.gradle.kts @@ -0,0 +1,19 @@ +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin + +plugins { + id("base-android-library") + id("de.gematik.ti.erp.names") +} + +val gematik = AppDependencyNamesPlugin() + +android { + namespace = gematik.moduleName("test_tags") + defaultConfig { + testApplicationId = gematik.moduleName("test_tags.test") + } +} + +dependencies { + implementation(libs.compose.ui) +} diff --git a/app/shared-test/consumer-rules.pro b/app/test-tags/consumer-rules.pro similarity index 100% rename from app/shared-test/consumer-rules.pro rename to app/test-tags/consumer-rules.pro diff --git a/app/shared-test/proguard-rules.pro b/app/test-tags/proguard-rules.pro similarity index 100% rename from app/shared-test/proguard-rules.pro rename to app/test-tags/proguard-rules.pro diff --git a/app/test-tags/src/main/AndroidManifest.xml b/app/test-tags/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5c3d3655 --- /dev/null +++ b/app/test-tags/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt b/app/test-tags/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt similarity index 90% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt rename to app/test-tags/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt index 89ea15f3..aa029c23 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt +++ b/app/test-tags/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app @@ -35,7 +35,7 @@ import kotlin.properties.ReadOnlyProperty */ fun tagName(): ReadOnlyProperty = ReadOnlyProperty { thisRef, property -> - "${thisRef!!::class.qualifiedName!!.removePrefix("de.gematik.ti.erp.app.TestTag.")}.${property.name}" + "${thisRef!!::class.qualifiedName!!.removePrefix("de.gematik.ti.erp.app.TestTag")}.${property.name}" } fun testDataPropertyKey(name: String) = @@ -92,6 +92,9 @@ object TestTag { val PrescriptionWaitForResponse by tagName() val PrescriptionInProgress by tagName() val PrescriptionRedeemed by tagName() + val PrescriptionProvided by tagName() + val PrescriptionDeleted by tagName() + val LocalRedeemScreen by tagName() val ArchiveButton by tagName() @@ -106,11 +109,14 @@ object TestTag { val MoreButton by tagName() val DeleteButton by tagName() + val SubstitutionButton by tagName() val MedicationButton by tagName() val PrescriberButton by tagName() val PatientButton by tagName() val OrganizationButton by tagName() val TechnicalInformationButton by tagName() + val PrescriptionDetailBottomSheetTitle by tagName() + val PrescriptionDetailBottomSheetDetail by tagName() object TechnicalInformation { val Content by tagName() @@ -274,6 +280,8 @@ object TestTag { val InsuranceSelectionContent by tagName() val ListOfInsuranceButtons by tagName() } + + val LanguageColumnList by tagName() } object AlertDialog { @@ -341,8 +349,10 @@ object TestTag { val MainScreen by tagName() val LoginButton by tagName() val CenterScreenMessageField by tagName() + val Avatar by tagName() val AddProfileButton by tagName() + object MainScreenBottomSheet { val Modal by tagName() val ProfileNameField by tagName() @@ -370,7 +380,10 @@ object TestTag { val InvoicesScreen by tagName() val InvoicesDetailScreen by tagName() val InvoicesScreenContent by tagName() + val InvoiceShareScreen by tagName() val OpenTokensScreenButton by tagName() + val InsurantName by tagName() + val InsuranceName by tagName() val InsuranceId by tagName() val LoginButton by tagName() val ThreeDotMenuButton by tagName() @@ -386,6 +399,9 @@ object TestTag { val ColorSelectorPinkButton by tagName() val ColorSelectorTreeButton by tagName() val ColorSelectorBlueMoonButton by tagName() + val DeleteAvatarButton by tagName() + val AvatarSelectorRow by tagName() + val EmojiSaveButton by tagName() } val OpenAuditEventsScreenButton by tagName() diff --git a/build.gradle.kts b/build.gradle.kts index 029a61e0..a7b55c1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,98 +1,100 @@ -import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask - -// NOTE: Only pre-include plugins (apply false) required by the modules android, common -// and desktop within this block to keep them excluded from the root module. -// If the plugin can't be resolved add a custom resolution strategy to `settings.gradle.kts`. -plugins { - - // Running `./gradlew dependencyUpdates` looks for the latest libs that are available - id("com.github.ben-manes.versions") version "0.48.0" - - id("org.owasp.dependencycheck") version "8.0.2" apply false - - // generates licence report - id("com.jaredsburrows.license") version "0.8.90" apply false - - id("com.android.application") version "8.1.0" apply false - - id("com.android.library") version "8.1.0" apply false - - kotlin("multiplatform") version "1.9.10" apply false +@file:Suppress("SpreadOperator") - kotlin("plugin.serialization") version "1.8.10" apply false - - id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false - - id("io.realm.kotlin") version "1.7.1" apply false - - // TODO: Update to latest version : https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#kotlin-compatibility - id("org.jetbrains.kotlin.android") version "1.9.10" apply false - - id("org.jetbrains.compose") version "1.5.3" apply false - - id("com.codingfeline.buildkonfig") version "0.13.3" apply false +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import java.util.Properties - id("io.gitlab.arturbosch.detekt") version "1.22.0" +private val ktlintMain: Configuration by configurations.creating +private val ktlintRules: Configuration by configurations.creating - id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false +plugins { - // Generate a html file of all referenced technical requirements + // module plugins + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + + // extra plugins + alias(libs.plugins.github.ben.manes.version) apply false + /** todo issue to be fixed: + * + * > Could not resolve all task dependencies for configuration ':classpath'. + * > Could not find org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2. + * Required by: + * project : > com.jaredsburrows.license:com.jaredsburrows.license.gradle.plugin:0.8.90 > com.jaredsburrows:gradle-license-plugin:0.8.90 + */ + // alias(libs.plugins.jaredburrows.license) apply false + alias(libs.plugins.gradle.secrets) apply false + alias(libs.plugins.realm.kotlin) apply false + alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.jetbrains.compose) apply false + alias(libs.plugins.buildkonfig) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.detekt) + + // test + alias(libs.plugins.paparazzi) apply false + id("jacoco") + + // custom gematik plugins id("de.gematik.ti.erp.technical-requirements") - - // Updates the version from the git tags - id("de.gematik.ti.erp.versioning") apply true - - // Updates the gradle.propertied - id("de.gematik.ti.erp.properties") apply true - - // Builds the different app flavours - id("de.gematik.ti.erp.flavours") apply true - - // Sends information to teams - id("de.gematik.ti.erp.teams") apply true + id("de.gematik.ti.erp.versioning") + id("de.gematik.ti.erp.properties") + id("de.gematik.ti.erp.flavours") + id("de.gematik.ti.erp.teams") + id("de.gematik.ti.erp.names") } -val ktlintMain: Configuration by configurations.creating -val ktlintRules: Configuration by configurations.creating - -dependencies { - ktlintMain("com.pinterest:ktlint:0.46.1") { - attributes { - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) +// obtain libs from nexus +try { + val properties = Properties() + properties.load(File("ci-overrides.properties").inputStream()) + val nexusUsername: String? = properties.getProperty("NEXUS_USERNAME") + val nexusPassword: String? = properties.getProperty("NEXUS_PASSWORD") + val nexusUrl: String? = properties.getProperty("NEXUS_URL") + + allprojects { + repositories { + if (!nexusUsername.isNullOrEmpty() && !nexusPassword.isNullOrEmpty() && !nexusUrl.isNullOrEmpty()) { + maven { + name = "nexus" + setUrl(nexusUrl) + credentials { + username = nexusUsername + password = nexusPassword + } + } + } } } - ktlintRules("de.gematik.ti.erp.app:rules:1.0") +} catch (e: Throwable) { + println("No ci-overrides.properties found") } val sourcesKt = listOf( - "app/android/src/**/de/gematik/**/*.kt", - "app/features/src/**/de/gematik/**/*.kt", - "app/demo-mode/src/**/de/gematik/**/*.kt", - "common/src/**/de/gematik/**/*.kt", - "desktop/src/**/de/gematik/**/*.kt", - "rules/src/**/de/gematik/**/*.kt", - "smartcard-wrapper/src/**/de/gematik/**/*.kt", - "plugins/*/src/**/*.kt" + "app/android/src/**/*.kt", + "app/android-mock/src/**/*.kt", + "app/demo-mode/src/**/*.kt", + "app/features/src/**/*.kt", + "app/test-actions/src/**/*.kt", + "app/test-tags/src/**/*.kt", + "buildSrc/src/**/*.kt", + "common/src/**/*.kt", + "desktop/src/**/*.kt", + "plugins/*/src/**/*.kt", + "rules/src/**/*.kt", + "scripts/src/**/*.kt", + "smartcard-wrapper/src/**/*.kt", + "ui-components/src/**/*.kt", ) -detekt { - autoCorrect = false - source = - fileTree(rootDir) { - include(sourcesKt) - }.filter { it.extension != "kts" } - .map { it.parentFile } - .let { - files(*it.toTypedArray()) - } - parallel = true - config = files("config/detekt/detekt.yml") - baseline = file("config/detekt/baseline.xml") - buildUponDefaultConfig = false - allRules = false - disableDefaultRuleSets = false - debug = false - ignoreFailures = false +dependencies { + ktlintMain(libs.quality.ktlint) { + attributes { + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) + } + } + ktlintRules(libs.quality.rules) } fun ktlintCreating(format: Boolean, sources: List, disableLicenceRule: Boolean) = @@ -106,7 +108,7 @@ fun ktlintCreating(format: Boolean, sources: List, disableLicenceRule: B if (disableLicenceRule) add("--disabled_rules=custom:licence-header") } // required for java > 16; see https://github.com/pinterest/ktlint/issues/1195 - jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED") } val ktlint by ktlintCreating(format = false, sources = sourcesKt, disableLicenceRule = false) @@ -130,3 +132,12 @@ tasks.withType { isUnstable(candidate.version) && !isUnstable(currentVersion) } } + +// config changes for security vulnerabilities +allprojects { + configurations.all { + resolutionStrategy { + force("com.google.guava:guava:33.2.0-jre") + } + } +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 4e3747b3..e91ef967 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,54 +1,61 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * ${GEMATIK_COPYRIGHT_STATEMENT} */ plugins { `kotlin-dsl` id("org.jetbrains.kotlin.jvm") version "1.9.0" + id("org.jetbrains.kotlin.plugin.serialization") version "1.9.10" } +fun implementationClass(value: String) = "de.gematik.ti.erp.app.plugins.$value" +fun name(value: String) = "de.gematik.ti.erp.$value" + gradlePlugin { plugins.create("versionApp") { - id = "de.gematik.ti.erp.versioning" - implementationClass = "de.gematik.ti.erp.app.plugins.buildapp.VersionAppPlugin" + id = name("versioning") + group = name("versioning") + implementationClass = implementationClass("buildapp.VersionAppPlugin") } plugins.create("updateGradleProperties") { - id = "de.gematik.ti.erp.properties" - implementationClass = "de.gematik.ti.erp.app.plugins.buildapp.UpdatePropertiesPlugin" + id = name("properties") + group = name("properties") + implementationClass = implementationClass("updateproperties.UpdatePropertiesPlugin") } plugins.create("buildAppFlavours") { - id = "de.gematik.ti.erp.flavours" - implementationClass = "de.gematik.ti.erp.app.plugins.buildapp.BuildAppFlavoursPlugin" + id = name("flavours") + group = name("flavours") + implementationClass = implementationClass("buildapp.BuildAppFlavoursPlugin") } plugins.create("teamsCommunication") { - id = "de.gematik.ti.erp.teams" - implementationClass = "de.gematik.ti.erp.app.plugins.teams.TeamsCommunicationPlugin" + id = name("teams") + group = name("teams") + implementationClass = implementationClass("teams.TeamsCommunicationPlugin") + } + plugins.create("dependencies") { + id = name("dependency-overrides") + group = name("dependency-overrides") + implementationClass = implementationClass("dependencies.DependenciesPlugin") + } + plugins.create("names") { + id = name("names") + group = name("names") + implementationClass = implementationClass("names.AppDependencyNamesPlugin") } } repositories { mavenCentral() + google() } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") - implementation("org.apache.httpcomponents.client5:httpclient5:5.3") - implementation("org.apache.httpcomponents.client5:httpclient5-fluent:5.3") - implementation("com.opencsv:opencsv:5.5.2") - testImplementation("junit:junit:4.13.2") - testImplementation("io.mockk:mockk:1.13.8") + implementation(libs.kotlinx.datetime) + implementation(libs.network.httpclient5) + implementation(libs.network.httpclient5.fluent) + implementation(libs.opencsv) + testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) + implementation(libs.dependency.check.gradle) + implementation(libs.kotlinx.serialization.json) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..5b483bce --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,14 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/ErpPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/ErpPlugin.kt index 9fc857df..5ac2a270 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/ErpPlugin.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/ErpPlugin.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/BuildAppFlavoursPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/BuildAppFlavoursPlugin.kt index d6457e62..6176cce4 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/BuildAppFlavoursPlugin.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/BuildAppFlavoursPlugin.kt @@ -1,67 +1,65 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.plugins.buildapp import de.gematik.ti.erp.app.ErpPlugin -import de.gematik.ti.erp.app.tasks.registerBuildAppGalleryBundle -import de.gematik.ti.erp.app.tasks.registerBuildDebugApp -import de.gematik.ti.erp.app.tasks.registerBuildGoogleTuApp -import de.gematik.ti.erp.app.tasks.registerBuildKonnyApp -import de.gematik.ti.erp.app.tasks.registerBuildMinifiedApp -import de.gematik.ti.erp.app.tasks.registerBuildMinifiedKonnyApp -import de.gematik.ti.erp.app.tasks.registerBuildMockApp -import de.gematik.ti.erp.app.tasks.registerBuildPlayStoreApp -import de.gematik.ti.erp.app.tasks.registerBuildPlayStoreBundle -import de.gematik.ti.erp.app.tasks.registerCopyAppGalleryBundle -import de.gematik.ti.erp.app.tasks.registerCopyDebugApp -import de.gematik.ti.erp.app.tasks.registerCopyGoogleTuApp -import de.gematik.ti.erp.app.tasks.registerCopyKonnyApp -import de.gematik.ti.erp.app.tasks.registerCopyMinifiedApp -import de.gematik.ti.erp.app.tasks.registerCopyMockApp -import de.gematik.ti.erp.app.tasks.registerCopyPlayStoreBundle +import de.gematik.ti.erp.app.tasks.buildAppGalleryBundle +import de.gematik.ti.erp.app.tasks.buildDebugApp +import de.gematik.ti.erp.app.tasks.buildGoogleTuApp +import de.gematik.ti.erp.app.tasks.buildKonnyApp +import de.gematik.ti.erp.app.tasks.buildMinifiedApp +import de.gematik.ti.erp.app.tasks.buildMinifiedKonnyApp +import de.gematik.ti.erp.app.tasks.buildMockApp +import de.gematik.ti.erp.app.tasks.buildPlayStoreApp +import de.gematik.ti.erp.app.tasks.buildPlayStoreBundle +import de.gematik.ti.erp.app.tasks.copyAppGalleryBundle +import de.gematik.ti.erp.app.tasks.copyDebugApp +import de.gematik.ti.erp.app.tasks.copyGoogleTuApp +import de.gematik.ti.erp.app.tasks.copyKonnyApp +import de.gematik.ti.erp.app.tasks.copyMinifiedApp +import de.gematik.ti.erp.app.tasks.copyMockApp +import de.gematik.ti.erp.app.tasks.copyPlayStoreBundle import org.gradle.api.Project @Suppress("unused") class BuildAppFlavoursPlugin : ErpPlugin { override fun apply(project: Project) { - project.tasks.apply { - // build tasks - registerBuildPlayStoreBundle() - registerBuildPlayStoreApp() - registerBuildAppGalleryBundle() - registerBuildGoogleTuApp() - registerBuildKonnyApp() - registerBuildDebugApp() - registerBuildMockApp() - registerBuildMinifiedApp() - registerBuildMinifiedKonnyApp() + buildPlayStoreBundle() + buildPlayStoreApp() + buildAppGalleryBundle() + buildGoogleTuApp() + buildKonnyApp() + buildDebugApp() + buildMockApp() + buildMinifiedApp() + buildMinifiedKonnyApp() // copy files tasks - registerCopyPlayStoreBundle() - registerCopyAppGalleryBundle() - registerCopyKonnyApp() - registerCopyGoogleTuApp() - registerCopyDebugApp() - registerCopyMockApp() - registerCopyMinifiedApp() + copyPlayStoreBundle() + copyAppGalleryBundle() + copyKonnyApp() + copyGoogleTuApp() + copyDebugApp() + copyMockApp() + copyMinifiedApp() } } diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/UpdatePropertiesPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/UpdatePropertiesPlugin.kt deleted file mode 100644 index ccb2b74f..00000000 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/UpdatePropertiesPlugin.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.plugins.buildapp - -import de.gematik.ti.erp.app.ErpPlugin -import de.gematik.ti.erp.app.utils.GRADLE_ERROR -import de.gematik.ti.erp.app.utils.GRADLE_USER_AGENT -import de.gematik.ti.erp.app.utils.GRADLE_VERSION_CODE -import de.gematik.ti.erp.app.utils.GRADLE_VERSION_NAME -import de.gematik.ti.erp.app.utils.TaskNames -import de.gematik.ti.erp.app.utils.VERSION_EXCEPTION -import de.gematik.ti.erp.app.utils.extractVersion -import de.gematik.ti.erp.app.utils.versionCode -import de.gematik.ti.erp.app.utils.versionName -import org.gradle.api.GradleScriptException -import org.gradle.api.Project -import org.gradle.api.Task -import java.util.Properties - -@Suppress("unused") -class UpdatePropertiesPlugin : ErpPlugin { - override fun apply(project: Project) { - project.tasks.register(TaskNames.updateGradleProperties) { - runDependencyTasks() - val properties = Properties() - doLast { - val versionCode = project.versionCode() - val versionName = project.versionName()?.extractVersion() - - val gradlePropertiesFile = project.file("gradle.properties") - properties.load(gradlePropertiesFile.reader()) - - if (versionName == null || versionCode == null) { - throw GradleScriptException(GRADLE_ERROR, Exception(VERSION_EXCEPTION)) - } - - properties.setProperty(GRADLE_USER_AGENT, "eRp-App-Android/$versionName GMTIK/eRezeptApp") - properties.setProperty(GRADLE_VERSION_CODE, versionCode.toString()) - properties.setProperty(GRADLE_VERSION_NAME, versionName) - - println("Gradle properties updated.") - - properties.store(gradlePropertiesFile.writer(), null) - } - } - } - - private fun Task.runDependencyTasks() { - dependsOn(TaskNames.versionApp) - } -} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/VersionAppPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/VersionAppPlugin.kt index 1caeae11..c798c742 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/VersionAppPlugin.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/buildapp/VersionAppPlugin.kt @@ -1,24 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.plugins.buildapp import de.gematik.ti.erp.app.ErpPlugin +import de.gematik.ti.erp.app.utils.GRADLE_PROPERTIES_FILE +import de.gematik.ti.erp.app.utils.OS_NAME import de.gematik.ti.erp.app.utils.SHELL_RES_FOLDER import de.gematik.ti.erp.app.utils.TaskNames import de.gematik.ti.erp.app.utils.VERSION_CODE_STRING @@ -26,12 +28,12 @@ import de.gematik.ti.erp.app.utils.VERSION_ERROR import de.gematik.ti.erp.app.utils.VERSION_NAME_EXCEPTION import de.gematik.ti.erp.app.utils.VERSION_NAME_STRING import de.gematik.ti.erp.app.utils.VERSION_PATTERN_EXCEPTION -import de.gematik.ti.erp.app.utils.OS_NAME import de.gematik.ti.erp.app.utils.WINDOWS import de.gematik.ti.erp.app.utils.doesNotHaveVersionCodeName import de.gematik.ti.erp.app.utils.execute import de.gematik.ti.erp.app.utils.isInvalidVersioningPattern import de.gematik.ti.erp.app.utils.isVersionNameEmpty +import de.gematik.ti.erp.app.utils.letNotNull import de.gematik.ti.erp.app.utils.splitForVersionParts import de.gematik.ti.erp.app.utils.versionCode import de.gematik.ti.erp.app.utils.versionName @@ -41,7 +43,6 @@ import org.gradle.api.Task import org.gradle.kotlin.dsl.extra import java.io.ByteArrayOutputStream - @Suppress("unused") class VersionAppPlugin : ErpPlugin { @@ -64,29 +65,66 @@ class VersionAppPlugin : ErpPlugin { } } doLast { - // sanity checks for version pattern + // If existing gradle.properties file contains a value higher than the one from git, it will be used val parts = outputText.splitForVersionParts() - if (parts.doesNotHaveVersionCodeName()) { - throw GradleScriptException( - VERSION_ERROR, Exception( - "$VERSION_PATTERN_EXCEPTION $outputText" - ) - ) - } - if (parts.versionName().isInvalidVersioningPattern()) { - throw GradleScriptException( - VERSION_ERROR, Exception( - "$VERSION_PATTERN_EXCEPTION ${parts.versionName()}" - ) - ) - } + val existingVersionName = project.readVersionNameFromProperties() + val existingVersionCode = project.readVersionCodeFromProperties() + if (parts.isVersionNameEmpty()) { throw GradleScriptException(VERSION_ERROR, Exception(VERSION_NAME_EXCEPTION)) } - project.extra[VERSION_CODE_STRING] = parts.versionCode() - project.extra[VERSION_NAME_STRING] = parts.versionName() - println("Versioning from given branch and tag done. $outputText") + val gitVersionName = parts.versionName() + + val semVerComparisonResult = compareSemver( + gradleVersion = existingVersionName ?: "0.0.0", + gitVersion = gitVersionName + ) + + when (semVerComparisonResult) { + SemVer.GRADLE, SemVer.SAME -> { + letNotNull(existingVersionName, (existingVersionCode)) { versionName, versionCode -> + println("Updating version name from gradle.properties as $versionName") + + // even on gradle update use the greater version code + val gradleVersionCode = versionCode.toInt() + val gitVersionCode = parts.versionCode() + val newVersionCode = maxOf(gradleVersionCode, gitVersionCode) + + println("Updating version code as $newVersionCode") + + project.extra[VERSION_CODE_STRING] = newVersionCode + project.extra[VERSION_NAME_STRING] = versionName + } ?: throw GradleScriptException( + VERSION_ERROR, + Exception( + "Version code or name not found in gradle.properties" + ) + ) + } + + SemVer.GIT -> { + if (parts.doesNotHaveVersionCodeName()) { + throw GradleScriptException( + VERSION_ERROR, + Exception( + "$VERSION_PATTERN_EXCEPTION $outputText" + ) + ) + } + if (parts.versionName().isInvalidVersioningPattern()) { + throw GradleScriptException( + VERSION_ERROR, + Exception( + "$VERSION_PATTERN_EXCEPTION ${parts.versionName()}" + ) + ) + } + print("Updating values from git $outputText") + project.extra[VERSION_CODE_STRING] = parts.versionCode() + project.extra[VERSION_NAME_STRING] = parts.versionName() + } + } } } @@ -108,4 +146,87 @@ class VersionAppPlugin : ErpPlugin { private fun Task.runDependencyTasks() { dependsOn(TaskNames.versionApp) } + + private fun Project.gradlePropertiesFile() = file(GRADLE_PROPERTIES_FILE) + + private fun Project.readVersionNameFromProperties() = + gradlePropertiesFile().useLines { + it.firstOrNull { value -> value.startsWith(VERSION_NAME_LABEL) } + }?.split("=")?.get(1)?.trim() + + private fun Project.readVersionCodeFromProperties() = + gradlePropertiesFile().useLines { + it.firstOrNull { value -> value.startsWith(VERSION_CODE_LABEL) } + }?.split("=")?.get(1)?.trim() + + private fun compareSemver(gradleVersion: String, gitVersion: String): SemVer { + val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?$") + val gradleVersionMatch = regex.matchEntire(gradleVersion) + val gitVersionMatch = regex.matchEntire(gitVersion) + + if (gradleVersionMatch != null && gitVersionMatch != null) { + val (majorGradle, minorGradle, patchGradle, preReleaseGradle) = gradleVersionMatch.destructured + val (majorGit, minorGit, patchGit, preReleaseGit) = gitVersionMatch.destructured + + if (majorGradle.toInt() != majorGit.toInt()) return if (majorGradle.toInt() > majorGit.toInt()) SemVer.GRADLE else SemVer.GIT + if (minorGradle.toInt() != minorGit.toInt()) return if (minorGradle.toInt() > minorGit.toInt()) SemVer.GRADLE else SemVer.GIT + if (patchGradle.toInt() != patchGit.toInt()) return if (patchGradle.toInt() > patchGit.toInt()) SemVer.GRADLE else SemVer.GIT + + // Compare pre-release versions if they exist + if (preReleaseGradle.isNotEmpty() || preReleaseGit.isNotEmpty()) { + val prePartsGradle = preReleaseGradle.split(".") + val prePartsGit = preReleaseGit.split(".") + val preComparison = comparePreRelease(prePartsGradle, prePartsGit) + return when { + preComparison > 0 -> SemVer.GRADLE + preComparison < 0 -> SemVer.GIT + else -> SemVer.SAME + } + } + + return SemVer.SAME + } else { + println("Invalid version on git $gitVersion") + return SemVer.GRADLE + } + } + + @Suppress("MagicNumber") + private fun comparePreRelease(preGradle: List, preGit: List): Int { + val maxLength = maxOf(preGradle.size, preGit.size) + for (i in 0 until maxLength) { + val valGradle = preGradle.getOrElse(i) { "" } + val valGit = preGit.getOrElse(i) { "" } + val numGradle = valGradle.toIntOrNull() + val numGit = valGit.toIntOrNull() + + when { + numGradle != null && numGit != null -> { // Both are numbers + if (numGradle != numGit) return numGradle.compareTo(numGit) + } + + numGradle != null -> return -1 // Numbers have lower precedence + numGit != null -> return 1 + else -> { // Both are strings + val comp = valGradle.compareTo(valGit) + if (comp != 0) return comp + } + } + } + + return preGradle.size.compareTo(preGit.size) // Fewer elements means lower precedence + } + + private fun maxOf(a: Int, b: Int): Int { + return if (a > b) a else b + } + + enum class SemVer { + GRADLE, GIT, SAME + } + + companion object { + private const val VERSION_NAME_LABEL = "VERSION_NAME=" + private const val VERSION_CODE_LABEL = "VERSION_CODE=" + } } diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/dependencies/DependenciesPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/dependencies/DependenciesPlugin.kt new file mode 100644 index 00000000..3cc7a815 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/dependencies/DependenciesPlugin.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.plugins.dependencies + +import de.gematik.ti.erp.app.ErpPlugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getPlugin +import java.util.Properties +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty + +class DependenciesPlugin : ErpPlugin { + val overrideProperties = Properties() + val gradleProperties = Properties() + override fun apply(project: Project) { + val ciPropertiesFile = project.rootProject.file("ci-overrides.properties") + if (ciPropertiesFile.exists()) { + overrideProperties.load(ciPropertiesFile.inputStream()) + } + + val gradlePropertiesFile = project.rootProject.file("gradle.properties") + if (gradlePropertiesFile.exists()) { + gradleProperties.load(gradlePropertiesFile.inputStream()) + } + } +} + +fun Project.overrides(): PropertyDelegateProvider> { + return PropertyDelegateProvider { _: Any?, _ -> + ReadOnlyProperty { _, property -> + project.plugins.getPlugin(DependenciesPlugin::class) + .overrideProperties + .getProperty(property.name) + ?: (project.properties[property.name] as? String) + ?: run { + project.plugins.getPlugin(DependenciesPlugin::class) + .gradleProperties + .getProperty(property.name) + ?: (project.properties[property.name] as? String) + ?: "" + } + } + } +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/names/AppDependencyNamesPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/names/AppDependencyNamesPlugin.kt new file mode 100644 index 00000000..d90b8776 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/names/AppDependencyNamesPlugin.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.plugins.names + +import de.gematik.ti.erp.app.ErpPlugin +import org.gradle.api.Project + +class AppDependencyNamesPlugin : ErpPlugin { + val appNameSpace = "de.gematik.ti.erp.app" + val appId = "de.gematik.ti.erp.app" + + // duplicates from script + val minifiedDebug = "minifiedDebug" + + // module names + val feature = ":app:features" + val uiComponents = ":ui-components" + val demoMode = ":app:demo-mode" + val testActions = ":app:test-actions" + val testTags = ":app:test-tags" + val multiplatform = ":common" + + fun moduleName(value: String) = "$appNameSpace.$value" + fun idName(value: String) = "$appId.$value" + override fun apply(project: Project) { + // no-op + } +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/Models.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/Models.kt index 7f17a70f..ce42a2fd 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/Models.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/Models.kt @@ -1,32 +1,33 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.plugins.teams @JvmInline value class VersionName(val value: String) + @JvmInline value class VersionCode(val value: String) @JvmInline value class BranchName(val value: String) { fun gitlabLink(): String { - //From MR-1234 get only 1234 + // From MR-1234 get only 1234 val pullRequestLink = value.replace(Regex("[^0-9]"), "") return "https://gitlab.prod.ccs.gematik.solutions/git/erezept/app/erp-app-android/-/merge_requests/$pullRequestLink" } @@ -50,6 +51,7 @@ value class JobName(val value: String) { } } +// In jenkins file EarlyExit is given to inform that there is no link data class AppCenterInformation( val owner: String, val appName: String diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsCommunicationPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsCommunicationPlugin.kt index 83b56fe9..71e09f2d 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsCommunicationPlugin.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsCommunicationPlugin.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.plugins.teams @@ -87,7 +87,6 @@ class TeamsCommunicationPlugin : ErpPlugin { println("Executed command: ${commandLine.execute()}") project.extensions.extraProperties[PAYLOAD] = payload } - } catch (e: Throwable) { println(e.stackTraceToString()) throw GradleException(e.localizedMessage) diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsPayLoad.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsPayLoad.kt index 5db91c54..cb0c1bbb 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsPayLoad.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsPayLoad.kt @@ -1,9 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + package de.gematik.ti.erp.app.plugins.teams import java.time.LocalDateTime import java.time.format.DateTimeFormatter - data class TeamsPayLoad( val summary: String, val versionName: VersionName, @@ -14,7 +31,7 @@ data class TeamsPayLoad( val lastCommitMessage: LastCommitMessage, val appCenterInformation: AppCenterInformation, val buildNumber: BuildNumber, - val jobName: JobName?, + val jobName: JobName? ) { private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") @@ -25,7 +42,7 @@ data class TeamsPayLoad( "themeColor": "7DD700", "sections": [ { - "activityTitle": "📬 Version $versionName ($versionCode)", + "activityTitle": "📬 Version ${versionName.value} (${versionCode.value})", "activitySubtitle": "🕐 Built on ${currentTime.format(formatter)}", "activityImage": "https://www.apotheke-johannstadt.de/assets/images/5/Euclid%20gematik%20E-Rezept%20Logo%20vert%20rgb_ohneTitel-2ea7aa74.png", "facts": [ @@ -43,7 +60,7 @@ data class TeamsPayLoad( }, { "name": "🕹️ AppCenter Link:", - "value": "Please access this build using the link ${appCenterInformation.appCenterUrl()}" + "value": "${appCenterInformation.appCenterUrl()}" } ], "images": [ @@ -85,7 +102,16 @@ data class TeamsPayLoad( private fun AppCenterInformation.appCenterUrl( // TODO: Find out how to get the latest id from app-center -) = "https://install.appcenter.ms/orgs/$owner/apps/$appName/releases" +) = + if (appName == "EarlyExit") { + "no upload for EarlyExit" + } else { + "https://install.appcenter.ms/orgs/$owner/apps/$appName/releases" + } private fun AppCenterInformation.qrCodeUrl() = - "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${appCenterUrl()}" + if (appName == "EarlyExit") { + "no upload for EarlyExit" + } else { + "https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${appCenterUrl()}" + } diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsProjectInformation.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsProjectInformation.kt index 88a254b6..d08d3054 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsProjectInformation.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/teams/TeamsProjectInformation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.plugins.teams diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/updateproperties/UpdatePropertiesPlugin.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/updateproperties/UpdatePropertiesPlugin.kt new file mode 100644 index 00000000..60319029 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/plugins/updateproperties/UpdatePropertiesPlugin.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.plugins.updateproperties + +import de.gematik.ti.erp.app.ErpPlugin +import de.gematik.ti.erp.app.tasks.updateApoFzdApiKeyTask +import de.gematik.ti.erp.app.tasks.updateFdApiKeysTask +import de.gematik.ti.erp.app.tasks.updateGradleProperties +import org.gradle.api.Project + +@Suppress("unused") +class UpdatePropertiesPlugin : ErpPlugin { + override fun apply(project: Project) { + project.tasks.apply { + // User Agent and Version + updateGradleProperties() + // ERP Keys + updateFdApiKeysTask(project) + // APO FZD Keys + updateApoFzdApiKeyTask(project) + } + } +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/BuildTasks.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/BuildTasks.kt index b8224b75..8d987e8a 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/BuildTasks.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/BuildTasks.kt @@ -1,30 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.tasks -import de.gematik.ti.erp.app.utils.BUILD_EXCEPTION import de.gematik.ti.erp.app.plugins.buildapp.BuildAppFlavoursPlugin -import de.gematik.ti.erp.app.utils.GRADLE_ERROR +import de.gematik.ti.erp.app.utils.BUILD_EXCEPTION import de.gematik.ti.erp.app.utils.TaskNames -import de.gematik.ti.erp.app.utils.execute import de.gematik.ti.erp.app.utils.extractRCVersion import de.gematik.ti.erp.app.utils.extractVersion +import de.gematik.ti.erp.app.utils.letNotNull import de.gematik.ti.erp.app.utils.versionCode import de.gematik.ti.erp.app.utils.versionName import org.gradle.api.GradleScriptException @@ -35,206 +34,267 @@ import org.gradle.api.tasks.TaskContainer private const val GRADLE_APP_IDENTIFIER = "./gradlew :app:android:" private const val GRADLE_MOCK_APP_IDENTIFIER = "./gradlew :app:android-mock:" private const val GIT_HASH = "git-hash" +private const val FORCED_VERSION_CODE = "v-code" +private const val FORCED_VERSION_NAME = "v-name" -internal fun TaskContainer.registerBuildPlayStoreBundle() { +internal fun TaskContainer.buildPlayStoreBundle() { register(TaskNames.buildPlayStoreBundle) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.PlayStoreBundle doLast { - val (versionCode, versionName) = project.getVersionCodeName() - val gitHash = project.getGitHash() + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = false) project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, - versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", + versionName = versionName, buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyPlayStoreBundle) } } -internal fun TaskContainer.registerBuildPlayStoreApp() { +internal fun TaskContainer.buildPlayStoreApp() { register(TaskNames.buildPlayStoreApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.PlayStoreApk doLast { - val (versionCode, versionName) = project.getVersionCodeName() + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } } } -internal fun TaskContainer.registerBuildAppGalleryBundle() { +internal fun TaskContainer.buildAppGalleryBundle() { register(TaskNames.buildAppGalleryBundle) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.AppGalleryBundle doLast { - val (versionCode, versionName) = project.getVersionCodeName() - val gitHash = project.getGitHash() + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = false) project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, - versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", + versionName = versionName, buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyAppGalleryBundle) } } -internal fun TaskContainer.registerBuildGoogleTuApp() { +internal fun TaskContainer.buildGoogleTuApp() { register(TaskNames.buildGoogleTuApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.GoogleTuApk doLast { - val (versionCode, versionName) = project.getVersionCodeName() + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyGoogleTuApp) } } -internal fun TaskContainer.registerBuildKonnyApp() { +internal fun TaskContainer.buildKonnyApp() { register(TaskNames.buildKonnyApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.KonnyApk doLast { - val (versionCode, versionName) = project.getVersionCodeName(isReleaseCandidate = true) + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyKonnyApp) } } -internal fun TaskContainer.registerBuildDebugApp() { +internal fun TaskContainer.buildDebugApp() { register(TaskNames.buildDebugApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.DebugApk doLast { - val (versionCode, versionName) = project.getVersionCodeName(isReleaseCandidate = true) + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = buildScript( - versionCode = versionCode, - versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", - buildCondition = buildCondition - ).execute() + commandLine( + "bash", + "-c", + buildScript( + versionCode = versionCode, + versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", + buildCondition = buildCondition + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyDebugApp) } } -internal fun TaskContainer.registerBuildMockApp() { +internal fun TaskContainer.buildMockApp() { register(TaskNames.buildMockApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.MockApk doLast { - val (versionCode, versionName) = project.getVersionCodeName(isReleaseCandidate = true) + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( appIdentifier = GRADLE_MOCK_APP_IDENTIFIER, versionCode = versionCode, versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } finalizedBy(TaskNames.copyMockApp) } } -internal fun TaskContainer.registerBuildMinifiedApp() { +internal fun TaskContainer.buildMinifiedApp() { register(TaskNames.buildMinifiedApp) { runDependencyTasks() - val buildCondition = BuildAppFlavoursPlugin.BuildCondition.MinifiedDebugApk doLast { - val (versionCode, versionName) = project.getVersionCodeName(isReleaseCandidate = true) - val gitHash = project.getGitHash() - project.exec { - commandLine = - buildScript( - versionCode = versionCode, - versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", - buildCondition = buildCondition - ).execute() - standardOutput = System.out - } + executionOfBuildMinifiedApp() } finalizedBy(TaskNames.copyMinifiedApp) } } -internal fun TaskContainer.registerBuildMinifiedKonnyApp() { +internal fun Task.executionOfBuildMinifiedApp() { + val buildCondition = BuildAppFlavoursPlugin.BuildCondition.MinifiedDebugApk + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) + val gitHash = project.getGitHash() + project.exec { + commandLine( + "bash", + "-c", + buildScript( + versionCode = versionCode, + versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", + buildCondition = buildCondition + ) + ) + standardOutput = System.out + errorOutput = System.out + } +} + +internal fun TaskContainer.buildMinifiedKonnyApp() { register(TaskNames.buildMinifiedKonnyApp) { runDependencyTasks() val buildCondition = BuildAppFlavoursPlugin.BuildCondition.MinifiedKonnyApk doLast { - val (versionCode, versionName) = project.getVersionCodeName(isReleaseCandidate = true) + val (versionCode, versionName) = project.calculateVersionCodeName(isRC = true) val gitHash = project.getGitHash() project.exec { - commandLine = + commandLine( + "bash", + "-c", buildScript( versionCode = versionCode, versionName = if (gitHash.isNullOrEmpty()) versionName else "$versionName-$gitHash", buildCondition = buildCondition - ).execute() + ) + ) standardOutput = System.out + errorOutput = System.out } } } } +private fun Project.calculateVersionCodeName(isRC: Boolean = false): Pair = + try { + letNotNull(getForcedVersionCode(), getForcedVersionName()) { versionCode, versionName -> + val codeToGo = versionCode.toInt() + val nameToGo = if (isRC) versionName.extractRCVersion() else versionName.extractVersion() + print("Using given code and name $codeToGo $nameToGo") + if (nameToGo == null || codeToGo == 0) { + throw GradleScriptException("Error calculating version code name", Exception(BUILD_EXCEPTION)) + } + codeToGo to nameToGo + } ?: run { + val items = project.getVersionCodeName(isRC) + print("Using git code and name ${items.first} ${items.second}") + items + } + } catch (e: Exception) { + throw GradleScriptException("Error calculating version code name", e) + } + private fun Project.getVersionCodeName(isReleaseCandidate: Boolean = false): Pair { val versionName = if (isReleaseCandidate) versionName()?.extractRCVersion() else versionName()?.extractVersion() val versionCode = versionCode() if (versionName == null || versionCode == null) { - throw GradleScriptException(GRADLE_ERROR, Exception(BUILD_EXCEPTION)) + throw GradleScriptException("BuildTasks getVersionCodeName issue", Exception(BUILD_EXCEPTION)) } - println("versionCode = $versionCode") - println("versionName = $versionName") return versionCode to versionName } +// external values from jenkins / commandline private fun Project.getGitHash(): String? = findProperty(GIT_HASH) as? String +private fun Project.getForcedVersionName(): String? = findProperty(FORCED_VERSION_NAME) as? String +private fun Project.getForcedVersionCode(): String? = findProperty(FORCED_VERSION_CODE) as? String private fun Task.runDependencyTasks() { dependsOn(TaskNames.versionApp) @@ -247,9 +307,9 @@ private fun buildScript( versionName: String, buildCondition: BuildAppFlavoursPlugin.BuildCondition ) = - "bash $appIdentifier${buildCondition.assembleTask} " + - "-PVERSION_CODE=$versionCode " + - "-PVERSION_NAME=$versionName " + - "-Pbuildkonfig.flavor=${buildCondition.buildFlavour}" + "$appIdentifier${buildCondition.assembleTask} " + + "-PVERSION_CODE=$versionCode " + + "-PVERSION_NAME=$versionName " + + "-Pbuildkonfig.flavor=${buildCondition.buildFlavour} --no-daemon" // ./gradlew bundleGooglePuExternalRelease -PVERSION_CODE=1 -PVERSION_NAME=1.0.0 -Pbuildkonfig.flavor=googlePuExternal diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/CopyTasks.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/CopyTasks.kt index 338cdcf5..2f4d7403 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/CopyTasks.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/CopyTasks.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.tasks +import de.gematik.ti.erp.app.plugins.buildapp.BuildAppFlavoursPlugin import de.gematik.ti.erp.app.utils.APP_MOCK_PROJECT_NAME import de.gematik.ti.erp.app.utils.APP_PROJECT_NAME -import de.gematik.ti.erp.app.plugins.buildapp.BuildAppFlavoursPlugin import de.gematik.ti.erp.app.utils.GOOGLE_TU_EXTERNAL_MAPPING_PATH import de.gematik.ti.erp.app.utils.HUAWEI_STORE_BUNDLE_FILE import de.gematik.ti.erp.app.utils.HUAWEI_STORE_BUNDLE_PATH @@ -38,6 +38,7 @@ import de.gematik.ti.erp.app.utils.PLAY_STORE_BUNDLE_FILE import de.gematik.ti.erp.app.utils.PLAY_STORE_BUNDLE_PATH import de.gematik.ti.erp.app.utils.PLAY_STORE_MAPPING_PATH import de.gematik.ti.erp.app.utils.TU_EXTERNAL_APP_APK_FILE +import de.gematik.ti.erp.app.utils.TU_EXTERNAL_APP_APK_FILE_UNSIGNED import de.gematik.ti.erp.app.utils.TU_EXTERNAL_APP_APK_PATH import de.gematik.ti.erp.app.utils.TU_INTERNAL_APP_APK_FILE import de.gematik.ti.erp.app.utils.TU_INTERNAL_APP_APK_PATH @@ -49,7 +50,7 @@ import java.io.File import java.nio.file.Files import java.nio.file.Paths -internal fun TaskContainer.registerCopyPlayStoreBundle() { +internal fun TaskContainer.copyPlayStoreBundle() { register(TaskNames.copyPlayStoreBundle) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> @@ -57,7 +58,11 @@ internal fun TaskContainer.registerCopyPlayStoreBundle() { val sourceDir = androidProject.buildDir doLast { val inputFile = sourceDir.resolve("$PLAY_STORE_BUNDLE_PATH/$PLAY_STORE_BUNDLE_FILE") - project copyFileFrom inputFile + if (inputFile.exists()) { + project copyFileFrom inputFile + } else { + throw GradleScriptException("AAB not found", Exception("copyPlayStoreBundle failed")) + } val inputMappingFile = sourceDir.resolve("$PLAY_STORE_MAPPING_PATH/$MAPPING_FILE") inputMappingFile moveMappingFileAndRenameTo BuildAppFlavoursPlugin.MappingFileName.PlayStore.fileName() } @@ -67,7 +72,7 @@ internal fun TaskContainer.registerCopyPlayStoreBundle() { } } -internal fun TaskContainer.registerCopyAppGalleryBundle() { +internal fun TaskContainer.copyAppGalleryBundle() { register(TaskNames.copyAppGalleryBundle) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> @@ -75,7 +80,11 @@ internal fun TaskContainer.registerCopyAppGalleryBundle() { val sourceDir = androidProject.buildDir doLast { val inputFile = sourceDir.resolve("$HUAWEI_STORE_BUNDLE_PATH/$HUAWEI_STORE_BUNDLE_FILE") - project copyFileFrom inputFile + if (inputFile.exists()) { + project copyFileFrom inputFile + } else { + throw GradleScriptException("AAB not found", Exception("copyAppGalleryBundle failed")) + } val inputMappingFile = sourceDir.resolve("$HUAWEI_STORE_MAPPING_PATH/$MAPPING_FILE") inputMappingFile moveMappingFileAndRenameTo BuildAppFlavoursPlugin.MappingFileName.AppGallery.fileName() } @@ -85,7 +94,7 @@ internal fun TaskContainer.registerCopyAppGalleryBundle() { } } -internal fun TaskContainer.registerCopyKonnyApp() { +internal fun TaskContainer.copyKonnyApp() { register(TaskNames.copyKonnyApp) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> @@ -93,7 +102,11 @@ internal fun TaskContainer.registerCopyKonnyApp() { val sourceDir = androidProject.buildDir doLast { val inputFile = sourceDir.resolve("$KONNY_APP_APK_PATH/$KONNY_APP_APK_FILE") - project copyFileFrom inputFile + if (inputFile.exists()) { + project copyFileFrom inputFile + } else { + throw GradleScriptException("APK not found", Exception("copyKonnyApp failed")) + } } } ?: run { throw GradleScriptException("Project missing", Exception("Expected project not found")) @@ -101,7 +114,7 @@ internal fun TaskContainer.registerCopyKonnyApp() { } } -internal fun TaskContainer.registerCopyGoogleTuApp() { +internal fun TaskContainer.copyGoogleTuApp() { register(TaskNames.copyGoogleTuApp) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> @@ -109,7 +122,14 @@ internal fun TaskContainer.registerCopyGoogleTuApp() { val sourceDir = androidProject.buildDir doLast { val inputFile = sourceDir.resolve("$TU_EXTERNAL_APP_APK_PATH/$TU_EXTERNAL_APP_APK_FILE") - project copyFileFrom inputFile + if (inputFile.exists()) { + project copyFileFrom inputFile + } else { + val unsignedInputFile = + sourceDir.resolve("$TU_EXTERNAL_APP_APK_PATH/$TU_EXTERNAL_APP_APK_FILE_UNSIGNED") + project copyFileFrom unsignedInputFile + print("Tu app is unsigned, copying unsigned apk") + } val inputMappingFile = sourceDir.resolve("$GOOGLE_TU_EXTERNAL_MAPPING_PATH/$MAPPING_FILE") inputMappingFile moveMappingFileAndRenameTo BuildAppFlavoursPlugin.MappingFileName.TuExternal.fileName() } @@ -119,7 +139,7 @@ internal fun TaskContainer.registerCopyGoogleTuApp() { } } -internal fun TaskContainer.registerCopyDebugApp() { +internal fun TaskContainer.copyDebugApp() { register(TaskNames.copyDebugApp) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> @@ -136,7 +156,7 @@ internal fun TaskContainer.registerCopyDebugApp() { } } -internal fun TaskContainer.registerCopyMockApp() { +internal fun TaskContainer.copyMockApp() { register(TaskNames.copyMockApp) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_MOCK_PROJECT_NAME }?.let { androidProject -> @@ -152,7 +172,7 @@ internal fun TaskContainer.registerCopyMockApp() { } } -internal fun TaskContainer.registerCopyMinifiedApp() { +internal fun TaskContainer.copyMinifiedApp() { register(TaskNames.copyMinifiedApp) { project.makeAppOutputDirectoryIfNotExists() project.subprojects.find { it.name == APP_PROJECT_NAME }?.let { androidProject -> diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateApoFzdApiKeyTask.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateApoFzdApiKeyTask.kt new file mode 100644 index 00000000..e0e9a348 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateApoFzdApiKeyTask.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.tasks + +import com.opencsv.CSVParserBuilder +import com.opencsv.CSVReaderBuilder +import de.gematik.ti.erp.app.tasks.ApoFzdApiKeysGradle.PHARMACY_API_KEY +import de.gematik.ti.erp.app.tasks.ApoFzdApiKeysGradle.PHARMACY_API_KEY_TEST +import de.gematik.ti.erp.app.utils.API_TOKEN +import de.gematik.ti.erp.app.utils.CsvDataFunctions +import de.gematik.ti.erp.app.utils.TaskNames +import de.gematik.ti.erp.app.utils.UnescapedProperties +import de.gematik.ti.erp.app.utils.buildForScenario +import de.gematik.ti.erp.app.utils.execute +import de.gematik.ti.erp.app.utils.extractMajorVersion +import de.gematik.ti.erp.app.utils.getToken +import de.gematik.ti.erp.app.utils.sanitizeForGitLab +import de.gematik.ti.erp.app.utils.versionName +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskContainer +import java.io.BufferedWriter +import java.io.ByteArrayOutputStream +import java.io.FileWriter +import java.io.StringReader + +/** + *Task to update the API keys in the ci-overrides.properties file: + * * [./gradlew updateApoFzdApiKeys -Ptoken=token] (get the token from someone in the team) + */ +internal fun TaskContainer.updateApoFzdApiKeyTask(project: Project) { + register(TaskNames.updateApoFzdApiKeys) { + runDependencyTasks() + val properties = UnescapedProperties() + doLast { + val majorVersion = project.versionName()?.extractMajorVersion()?.sanitizeForGitLab() ?: "" + println("major-version $majorVersion") + val token = project.getToken() + if (token.isNullOrEmpty()) { + throw GradleException( + """ + Missing token, cannot update API keys. + Task needs to be runs as ./gradlew ${TaskNames.updateApoFzdApiKeys} -P$API_TOKEN=token + where the {token} is the api-token to the restricted repo + """ + .trimMargin() + ) + } + val ruCsvData by lazy { + project.getCsvDataAsString(token, ApoFzdApiKeyEnvironment.RU) + } + val puCsvData by lazy { + project.getCsvDataAsString(token, ApoFzdApiKeyEnvironment.PU) + } + val ruKey = getApovzdKey(ruCsvData, majorVersion) + val puKey = getApovzdKey(puCsvData, majorVersion) + + val ciOverridesPropertiesFile = project.file("ci-overrides.properties") + properties.load(ciOverridesPropertiesFile.reader()) + + if (ruKey.key.isNotEmpty()) { + properties.setProperty(PHARMACY_API_KEY_TEST.name, ruKey.key) + } else { + throw GradleException("Could not find the ApoFzd API keys RU for the major version $majorVersion") + } + + if (puKey.key.isNotEmpty()) { + properties.setProperty(PHARMACY_API_KEY.name, puKey.key) + } else { + throw GradleException("Could not find the ApoFzd API keys PU for the major version $majorVersion") + } + + if (ruKey.key.isNotEmpty() && puKey.key.isNotEmpty()) { + println("apofzd api keys updated") + } + + val writer = BufferedWriter(FileWriter(ciOverridesPropertiesFile)) + val time = getCurrentTimeFormatted() + properties.storeNoEscape(writer, "Last changed on $time") + writer.close() + } + } +} + +private fun Project.getCsvDataAsString( + token: String, + environment: ApoFzdApiKeyEnvironment +): String { + return ByteArrayOutputStream().use { outputStream -> + this.exec { + commandLine("curl", "-H", "PRIVATE-TOKEN: $token", apoFzdApiKey(environment.name)) + commandLine.execute() + standardOutput = outputStream + } + outputStream.toString() + } +} + +private fun getApovzdKey( + csvData: String, + majorVersion: String +): ApovzdKey { + var key = ApovzdKey() + val parser = CSVParserBuilder().buildForScenario() + val reader = CSVReaderBuilder(StringReader(csvData)) + .withCSVParser(parser) + .build() + reader.use { csvReader -> + csvReader.forEach { record -> + CsvDataFunctions.findKey( + record = record, + majorVersion = majorVersion, + key = ApoFzdApiKeyPlatform.Android.name + ) { apiKey -> + key = ApovzdKey(apiKey) + } + } + } + return key +} + +private fun apoFzdApiKey(environment: String) = + "https://gitlab.prod.ccs.gematik.solutions/api/v4/projects/965/repository/files/$environment.csv/raw?ref=main" + +private fun Task.runDependencyTasks() { + dependsOn(TaskNames.versionApp) +} + +enum class ApoFzdApiKeyPlatform { + Android +} + +enum class ApoFzdApiKeyEnvironment { + PU, RU +} + +enum class ApoFzdApiKeysGradle { + PHARMACY_API_KEY_TEST, + PHARMACY_API_KEY +} + +@JvmInline +value class ApovzdKey(val key: String = "") diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateFdApiKeyTask.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateFdApiKeyTask.kt new file mode 100644 index 00000000..5a39d24a --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateFdApiKeyTask.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.tasks + +import com.opencsv.CSVParserBuilder +import com.opencsv.CSVReaderBuilder +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_GOOGLE_PU +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_GOOGLE_RU +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_GOOGLE_TU +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_HUAWEI_PU +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_HUAWEI_RU +import de.gematik.ti.erp.app.tasks.FdErpApiKeysGradle.ERP_API_KEY_HUAWEI_TU +import de.gematik.ti.erp.app.utils.API_TOKEN +import de.gematik.ti.erp.app.utils.CsvDataFunctions +import de.gematik.ti.erp.app.utils.TaskNames +import de.gematik.ti.erp.app.utils.UnescapedProperties +import de.gematik.ti.erp.app.utils.buildForScenario +import de.gematik.ti.erp.app.utils.execute +import de.gematik.ti.erp.app.utils.extractMajorVersion +import de.gematik.ti.erp.app.utils.getToken +import de.gematik.ti.erp.app.utils.sanitizeForGitLab +import de.gematik.ti.erp.app.utils.versionName +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskContainer +import java.io.BufferedWriter +import java.io.ByteArrayOutputStream +import java.io.FileWriter +import java.io.StringReader +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + *Task to update the API keys in the ci-overrides.properties file: + * * [./gradlew updateFdApiKeys -Ptoken=token] (get the token from someone in the team) + */ +internal fun TaskContainer.updateFdApiKeysTask(project: Project) { + register(TaskNames.updateFdApiKeys) { + runDependencyTasks() + val properties = UnescapedProperties() + doLast { + val majorVersion = project.versionName()?.extractMajorVersion()?.sanitizeForGitLab() ?: "" + println("major-version $majorVersion") + val token = project.getToken() + if (!token.isNullOrEmpty()) { + val ruCsvData by lazy { + project.getCsvDataAsString(token, FdApiKeyEnvironment.RU) + } + val puCsvData by lazy { + project.getCsvDataAsString(token, FdApiKeyEnvironment.PU) + } + val tuCsvData by lazy { + project.getCsvDataAsString(token, FdApiKeyEnvironment.TU) + } + val ruKey = getAndroidHuaweiVersion(ruCsvData, majorVersion) + val puKey = getAndroidHuaweiVersion(puCsvData, majorVersion) + val tuKey = getAndroidHuaweiVersion(tuCsvData, majorVersion) + + val ciOverridesPropertiesFile = project.file("ci-overrides.properties") + properties.load(ciOverridesPropertiesFile.reader()) + + if (ruKey.isNotEmpty()) { + properties.setProperty(ERP_API_KEY_GOOGLE_RU.name, ruKey.android) + properties.setProperty(ERP_API_KEY_HUAWEI_RU.name, ruKey.huawei) + } else { + throw GradleException("Missing API keys for RU") + } + + if (tuKey.isNotEmpty()) { + properties.setProperty(ERP_API_KEY_GOOGLE_TU.name, tuKey.android) + properties.setProperty(ERP_API_KEY_HUAWEI_TU.name, tuKey.huawei) + } else { + throw GradleException("Missing API keys for TU") + } + + if (puKey.isNotEmpty()) { + properties.setProperty(ERP_API_KEY_GOOGLE_PU.name, puKey.android) + properties.setProperty(ERP_API_KEY_HUAWEI_PU.name, puKey.huawei) + } else { + throw GradleException("Missing API keys for PU") + } + + if (ruKey.isNotEmpty() && tuKey.isNotEmpty() && puKey.isNotEmpty()) { + println("erp api keys updated.") + } + + val writer = BufferedWriter(FileWriter(ciOverridesPropertiesFile)) + val time = getCurrentTimeFormatted() + properties.storeNoEscape(writer, "Last changed on $time") + writer.close() + } else { + throw GradleException( + """ + Missing token, cannot update API keys. + Task needs to be runs as ./gradlew ${TaskNames.updateFdApiKeys} -P$API_TOKEN=token + where the {token} is the api-token to the restricted repo + """ + .trimMargin() + ) + } + } + } +} + +private fun Project.getCsvDataAsString( + token: String, + environment: FdApiKeyEnvironment +): String { + return ByteArrayOutputStream().use { outputStream -> + this.exec { + commandLine("curl", "-H", "PRIVATE-TOKEN: $token", fdApiKeyUrl(environment.name)) + commandLine.execute() + standardOutput = outputStream + } + outputStream.toString() + } +} + +@Suppress("NestedBlockDepth") +private fun getAndroidHuaweiVersion( + csvData: String, + majorVersion: String +): FdEnvironmentApiKey { + var key = FdEnvironmentApiKey() + val parser = CSVParserBuilder().buildForScenario() + val reader = CSVReaderBuilder(StringReader(csvData)) + .withCSVParser(parser) + .build() + reader.use { csvReader -> + csvReader.forEach { record -> + CsvDataFunctions.findKey( + record = record, + majorVersion = majorVersion, + key = FdApiKeyPlatform.Android.name + ) { + key = key.copy(android = it) + } + CsvDataFunctions.findKey( + record = record, + majorVersion = majorVersion, + key = FdApiKeyPlatform.Huawei.name + ) { + key = key.copy(huawei = it) + } + } + } + return key +} + +private fun fdApiKeyUrl(environment: String) = + "https://gitlab.prod.ccs.gematik.solutions/api/v4/projects/961/repository/files/$environment.csv/raw?ref=main" + +private fun Task.runDependencyTasks() { + dependsOn(TaskNames.versionApp) +} + +enum class FdApiKeyEnvironment { + PU, TU, RU +} + +enum class FdApiKeyPlatform { + Android, Huawei +} + +enum class FdErpApiKeysGradle { + ERP_API_KEY_GOOGLE_RU, + ERP_API_KEY_HUAWEI_RU, + ERP_API_KEY_GOOGLE_TU, + ERP_API_KEY_HUAWEI_TU, + ERP_API_KEY_GOOGLE_PU, + ERP_API_KEY_HUAWEI_PU +} + +data class FdEnvironmentApiKey( + val android: String = "", + val huawei: String = "" +) { + fun isNotEmpty() = android.isNotEmpty() && huawei.isNotEmpty() +} + +fun getCurrentTimeFormatted(): String { + val dateFormat = SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.GERMANY) + return dateFormat.format(Date()) +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateGradlePropertyTask.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateGradlePropertyTask.kt new file mode 100644 index 00000000..13b6c9d4 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/tasks/UpdateGradlePropertyTask.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.tasks + +import de.gematik.ti.erp.app.utils.GRADLE_USER_AGENT +import de.gematik.ti.erp.app.utils.GRADLE_VERSION_CODE +import de.gematik.ti.erp.app.utils.GRADLE_VERSION_NAME +import de.gematik.ti.erp.app.utils.TaskNames +import de.gematik.ti.erp.app.utils.VERSION_EXCEPTION +import de.gematik.ti.erp.app.utils.extractRCVersion +import de.gematik.ti.erp.app.utils.extractVersion +import de.gematik.ti.erp.app.utils.versionCode +import de.gematik.ti.erp.app.utils.versionName +import org.gradle.api.GradleScriptException +import org.gradle.api.Task +import org.gradle.api.tasks.TaskContainer +import java.util.Properties + +internal fun TaskContainer.updateGradleProperties() { + register(TaskNames.updateGradleProperties) { + runDependencyTasks() + val properties = Properties() + doLast { + val versionCode = project.versionCode() + val versionName = project.versionName()?.extractRCVersion() + val userAgentVersionName = project.versionName()?.extractVersion() + + val gradlePropertiesFile = project.file("gradle.properties") + properties.load(gradlePropertiesFile.reader()) + + if (versionName == null || versionCode == null) { + throw GradleScriptException( + "UpdatePropertiesPlugin.updateGradleProperties", + Exception("$VERSION_EXCEPTION name=$versionName code=$versionCode") + ) + } + + properties.setProperty(GRADLE_USER_AGENT, "eRp-App-Android/$userAgentVersionName GMTIK") + properties.setProperty(GRADLE_VERSION_CODE, versionCode.toString()) + properties.setProperty(GRADLE_VERSION_NAME, versionName) + + println("Gradle properties updated.") + + properties.store(gradlePropertiesFile.writer(), null) + } + } +} + +private fun Task.runDependencyTasks() { + dependsOn(TaskNames.versionApp) +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Constants.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Constants.kt index 47131a63..ab88ce82 100644 --- a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Constants.kt +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Constants.kt @@ -1,33 +1,37 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("TooManyFunctions") package de.gematik.ti.erp.app.utils -import org.gradle.api.GradleException -import org.gradle.api.Project -import org.gradle.kotlin.dsl.extra - internal const val APP_PROJECT_NAME = "android" internal const val APP_MOCK_PROJECT_NAME = "android-mock" -private const val VERSION_PARTS = 4 -private const val VERSION_NAME_PART = 3 -private const val VERSION_CODE_PART = 1 +internal const val VERSION_PARTS = 4 +internal const val VERSION_NAME_PART = 3 +internal const val VERSION_CODE_PART = 1 + +//region csv data +internal const val ITEMS_IN_ROW = 5 +internal const val VERSION_ITEM = 4 +internal const val API_KEY_POSITION = 1 +//endregion + internal const val VERSION_CODE_STRING = "versionCode" internal const val VERSION_NAME_STRING = "versionName" internal const val LAST_MESSAGE_STRING = "lastCommitMessage" @@ -38,11 +42,10 @@ internal const val GRADLE_VERSION_NAME = "VERSION_NAME" internal const val VERSION_EXCEPTION = "Could not get the version name and code correctly" internal const val VERSION_PATTERN_EXCEPTION = "The created version code does not follow the allowed pattern of 1.19.1-RC1. " + - "The initial R is removed in the shell script" + "The initial R is removed in the shell script" internal const val VERSION_NAME_EXCEPTION = "Missing version name" internal const val BUILD_EXCEPTION = "Could not get the version name and code correctly to build the app" internal const val VERSION_ERROR = "Versioning error" -internal const val GRADLE_ERROR = "Versioning/UserAgent error" internal const val OUTPUT_DIRECTORY = "artifacts" internal const val PLAY_STORE_BUNDLE_PATH = "outputs/bundle/googlePuExternalRelease" internal const val HUAWEI_STORE_BUNDLE_PATH = "outputs/bundle/huaweiPuExternalRelease" @@ -56,6 +59,7 @@ internal const val HUAWEI_STORE_BUNDLE_FILE = "android-huaweiPuExternal-release. internal const val KONNY_APP_APK_FILE = "android-konnektathonRuInternal-debug.apk" internal const val TU_INTERNAL_APP_APK_FILE = "android-googleTuInternal-debug.apk" internal const val TU_EXTERNAL_APP_APK_FILE = "android-googleTuExternal-release.apk" +internal const val TU_EXTERNAL_APP_APK_FILE_UNSIGNED = "android-googleTuExternal-release-unsigned.apk" internal const val MOCK_APP_APK_FILE = "android-mock-debug.apk" internal const val MINIFIED_APP_APK_FILE = "android-googleTuInternal-minifiedDebug.apk" internal const val PLAY_STORE_MAPPING_PATH = "outputs/mapping/googlePuExternalRelease" @@ -66,72 +70,5 @@ internal const val MAPPING_FILE = "mapping.txt" internal const val PAYLOAD = "payload" internal const val OS_NAME = "os.name" internal const val WINDOWS = "win" - -internal fun String.splitForVersionParts() = this.split(":") -internal fun Project.versionCode() = extra[VERSION_CODE_STRING] as? Int -internal fun Project.versionName() = extra[VERSION_NAME_STRING] as? String -internal fun List.versionCode() = this[VERSION_CODE_PART].trim().toInt() -internal fun List.versionName() = this[VERSION_NAME_PART].trim() -internal fun List.isVersionNameEmpty() = this.versionName().isEmpty() -internal fun List.hasVersionCodeName() = this.size == VERSION_PARTS -internal fun List.doesNotHaveVersionCodeName() = !hasVersionCodeName() - -internal fun Project.lastCommit() = extra[LAST_MESSAGE_STRING] as? String - -// The script expects the string split in the end to execute the gradle task -internal fun String.execute() = split(" ") -internal fun (MutableList?).execute() = this?.joinToString(" ") - -internal fun String.isValidVersionCode(): Boolean { - // Checks if the version follows 1.10.2-RC1, the R is removed in the shell script - val pattern = Regex("\\d+\\.\\d+\\.\\d+-RC\\d+(-[a-zA-Z0-9]+)?$") - return pattern.matches(this) -} - -internal fun String.isInvalidVersioningPattern() = !isValidVersionCode() -internal fun String.extractVersion(): String? { - // Make R1.19.1-RC2 to 1.19.1 - val regex = Regex("\\d+\\.\\d+\\.\\d+") - val matchResult = regex.find(this) - return matchResult?.value -} - -internal fun String.extractRCVersion(): String { - // Make R1.19.1-RC2 to 1.19.1-RC2 - val regex = Regex("^R(.+)$") - return regex.replace(this, "$1") -} - -internal fun Project.detectPropertyOrNull(name: String) = findProperty(name) as? String -internal fun Project.detectPropertyOrThrow(name: String): String { - val property = findProperty(name) as? String - return property ?: throw GradleException("Missing argument $name") -} - -/** - * List of gradle tasks used for the build process - */ -object TaskNames { - const val versionApp = "versionApp" - const val printVersionCode = "printVersionCode" - const val printVersionName = "printVersionName" - const val updateGradleProperties = "updateGradleProperties" - const val buildPlayStoreBundle = "buildPlayStoreBundle" - const val buildPlayStoreApp = "buildPlayStoreApp" - const val buildAppGalleryBundle = "buildAppGalleryBundle" - const val buildGoogleTuApp = "buildGoogleTuApp" - const val buildKonnyApp = "buildKonnyApp" - const val buildDebugApp = "buildDebugApp" - const val buildMockApp = "buildMockApp" - const val buildMinifiedApp = "buildMinifiedApp" - const val buildMinifiedKonnyApp = "buildMinifiedKonnyApp" - const val copyPlayStoreBundle = "copyPlayStoreBundle" - const val copyAppGalleryBundle = "copyAppGalleryBundle" - const val copyGoogleTuApp = "copyGoogleTuApp" - const val copyKonnyApp = "copyKonnyApp" - const val copyDebugApp = "copyDebugApp" - const val copyMockApp = "copyMockApp" - const val copyMinifiedApp = "copyMinifiedApp" - const val sendTeamsNotification = "sendTeamsNotification" - const val lastMessage = "lastMessage" -} +internal const val GRADLE_PROPERTIES_FILE = "gradle.properties" +internal const val API_TOKEN = "token" diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/CsvDataFunctions.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/CsvDataFunctions.kt new file mode 100644 index 00000000..845b4c06 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/CsvDataFunctions.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +object CsvDataFunctions { + + @Suppress("NestedBlockDepth") + fun findKey( + record: Array, + majorVersion: String, + key: String, + apiKey: (String) -> Unit + ) { + if (record.size != ITEMS_IN_ROW) return + + val versionWithPlatform = record[VERSION_ITEM].split(" ") + if (versionWithPlatform.size < 2) return + + val (platform, version) = versionWithPlatform + if (platform == key && version.trim() == majorVersion) { + val apikey = record[API_KEY_POSITION] + println("ApiKey for $key is $apikey") + apiKey(apikey) + } + } +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Extensions.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Extensions.kt new file mode 100644 index 00000000..8c2b6144 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/Extensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +import com.opencsv.CSVParser +import com.opencsv.CSVParserBuilder + +inline fun letNotNull(first: A?, second: B?, transform: (A, B) -> R): R? = + if (first != null && second != null) { + transform(first, second) + } else { + null + } + +internal fun CSVParserBuilder.buildForScenario(): CSVParser = + withSeparator(',').withIgnoreQuotations(true).build() diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/ProjectExtensions.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/ProjectExtensions.kt new file mode 100644 index 00000000..06038f6e --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/ProjectExtensions.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra + +internal fun Project.versionCode() = extra[VERSION_CODE_STRING] as? Int + +internal fun Project.versionName() = extra[VERSION_NAME_STRING] as? String + +internal fun Project.lastCommit() = extra[LAST_MESSAGE_STRING] as? String + +internal fun Project.getToken(): String? = findProperty(API_TOKEN) as? String + +internal fun Project.detectPropertyOrNull(name: String) = findProperty(name) as? String + +internal fun Project.detectPropertyOrThrow(name: String): String { + val property = findProperty(name) as? String + return property ?: throw GradleException("Missing argument $name") +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt new file mode 100644 index 00000000..6f00eb1c --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +internal fun List.versionCode() = this[VERSION_CODE_PART].trim().toInt() +internal fun List.versionName() = this[VERSION_NAME_PART].trim() +internal fun List.isVersionNameEmpty() = this.versionName().isEmpty() +internal fun List.hasVersionCodeName() = this.size == VERSION_PARTS +internal fun List.doesNotHaveVersionCodeName() = !hasVersionCodeName() + +internal fun (MutableList?).execute() = this?.joinToString(" ") + +internal fun String.splitForVersionParts() = this.split(":") + +// The script expects the string split in the end to execute the gradle task +internal fun String.execute() = split(" ") + +internal fun String.isValidVersionCode(): Boolean { + // Checks if the version follows 1.10.2-RC1, the R is removed in the shell script + val pattern = Regex("\\d+\\.\\d+\\.\\d+-RC\\d+(-[a-zA-Z0-9]+)?$") + return pattern.matches(this) +} + +internal fun String.isInvalidVersioningPattern() = !isValidVersionCode() + +internal fun String.extractVersion(): String? { + // Make R1.19.1-RC2 to 1.19.1 + val regex = Regex("\\d+\\.\\d+\\.\\d+") + val matchResult = regex.find(this) + return matchResult?.value +} + +internal fun String.extractMajorVersion(): String? { + // Make R1.19.1-RC2 to 1.19 + val regex = Regex("\\d+\\.\\d+") + val matchResult = regex.find(this) + return matchResult?.value +} + +internal fun String.extractRCVersion(): String { + // Make R1.19.1-RC2 to 1.19.1-RC2 + val regex = Regex("^R(.+)$") + return regex.replace(this, "$1") +} + +internal fun String.sanitizeForGitLab() = "$this.0" diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/TaskNames.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/TaskNames.kt new file mode 100644 index 00000000..eef7f2e9 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/TaskNames.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +/** + * List of gradle tasks used for the build process + */ +object TaskNames { + const val versionApp = "versionApp" + const val printVersionCode = "printVersionCode" + const val printVersionName = "printVersionName" + const val updateGradleProperties = "updateGradleProperties" + const val buildPlayStoreBundle = "buildPlayStoreBundle" + const val buildPlayStoreApp = "buildPlayStoreApp" + const val buildAppGalleryBundle = "buildAppGalleryBundle" + const val buildGoogleTuApp = "buildGoogleTuApp" + const val buildKonnyApp = "buildKonnyApp" + const val buildDebugApp = "buildDebugApp" + const val buildMockApp = "buildMockApp" + const val buildMinifiedApp = "buildMinifiedApp" + const val buildMinifiedKonnyApp = "buildMinifiedKonnyApp" + const val copyPlayStoreBundle = "copyPlayStoreBundle" + const val copyAppGalleryBundle = "copyAppGalleryBundle" + const val copyGoogleTuApp = "copyGoogleTuApp" + const val copyKonnyApp = "copyKonnyApp" + const val copyDebugApp = "copyDebugApp" + const val copyMockApp = "copyMockApp" + const val copyMinifiedApp = "copyMinifiedApp" + const val sendTeamsNotification = "sendTeamsNotification" + const val lastMessage = "lastMessage" + const val updateFdApiKeys = "updateFdApiKeys" + const val updateApoFzdApiKeys = "updateApoFzdApiKeys" +} diff --git a/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/UnescapedProperties.kt b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/UnescapedProperties.kt new file mode 100644 index 00000000..88e6e5fe --- /dev/null +++ b/buildSrc/src/main/kotlin/de/gematik/ti/erp/app/utils/UnescapedProperties.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +import java.util.Properties + +/** + * Properties class that does not escape the values when storing them. + * By default, the store method escapes characters like = and : by adding a backslash before them. + * This class does not escape the values. + */ +class UnescapedProperties : Properties() { + override fun store(out: java.io.Writer?, comments: String?) { + val writer = out as? java.io.BufferedWriter ?: out?.let { java.io.BufferedWriter(it) } + if (writer != null) { + storeNoEscape(writer, comments) + } + } + + fun storeNoEscape(out: java.io.BufferedWriter, comments: String?) { + val lineSeparator = System.getProperty("line.separator") + if (comments != null) { + out.write("#$comments") + out.write(lineSeparator) + } + synchronized(this) { + for ((key, value) in this.entries) { + out.write(key.toString()) + out.write("=") + out.write(value.toString()) + out.write(lineSeparator) + } + } + out.flush() + } +} diff --git a/buildSrc/src/test/kotlin/de/gematik/ti/erp/app/ConstantTest.kt b/buildSrc/src/test/kotlin/de/gematik/ti/erp/app/ConstantTest.kt index 361cf772..4458d712 100644 --- a/buildSrc/src/test/kotlin/de/gematik/ti/erp/app/ConstantTest.kt +++ b/buildSrc/src/test/kotlin/de/gematik/ti/erp/app/ConstantTest.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app +import de.gematik.ti.erp.app.utils.extractMajorVersion import de.gematik.ti.erp.app.utils.extractRCVersion import de.gematik.ti.erp.app.utils.extractVersion import de.gematik.ti.erp.app.utils.isValidVersionCode @@ -47,4 +48,9 @@ class ConstantTest { Assert.assertEquals("1.19.1-RC2", "R1.19.1-RC2".extractRCVersion()) Assert.assertEquals("9.100.10-RC10", "R9.100.10-RC10".extractRCVersion()) } + + @Test + fun extractMajorVersionTest() { + Assert.assertEquals("1.19", "1.19.1-RC2".extractMajorVersion()) + } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c7c9bbb0..54217a6c 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,22 +1,17 @@ +@file:Suppress("UnusedPrivateProperty", "VariableNaming", "PropertyName") + import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.INT import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.LONG import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import de.gematik.ti.erp.overriding +import de.gematik.ti.erp.app.plugins.dependencies.overrides import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly import java.io.ByteArrayOutputStream plugins { - id("com.android.library") - kotlin("multiplatform") - kotlin("plugin.serialization") - id("io.realm.kotlin") - id("org.jetbrains.compose") + id("base-multiplatform-library") id("com.codingfeline.buildkonfig") - id("de.gematik.ti.erp.dependencies") - id("de.gematik.ti.erp.technical-requirements") + id("de.gematik.ti.erp.dependency-overrides") } fun getGitHash() = if (File("${rootDir.path}/.git").exists()) { @@ -29,99 +24,92 @@ fun getGitHash() = } else { "n/a" } -val USER_AGENT: String by overriding() -val DATA_PROTECTION_LAST_UPDATED: String by overriding() -val VERSION_CODE: String by overriding() -val VERSION_NAME: String by overriding() -val DEBUG_TEST_IDS_ENABLED: String by overriding() -val VAU_OCSP_RESPONSE_MAX_AGE: String by overriding() -val APP_TRUST_ANCHOR_BASE64: String by overriding() -val APP_TRUST_ANCHOR_BASE64_TEST: String by overriding() -val PHARMACY_SERVICE_URI: String by overriding() -val PHARMACY_SERVICE_URI_TEST: String by overriding() -val PHARMACY_API_KEY: String by overriding() -val PHARMACY_API_KEY_TEST: String by overriding() -val BASE_SERVICE_URI_PU: String by overriding() -val BASE_SERVICE_URI_TU: String by overriding() -val BASE_SERVICE_URI_RU: String by overriding() -val BASE_SERVICE_URI_RU_DEV: String by overriding() -val BASE_SERVICE_URI_TR: String by overriding() -val IDP_SERVICE_URI_PU: String by overriding() -val IDP_SERVICE_URI_TU: String by overriding() -val IDP_SERVICE_URI_RU: String by overriding() -val IDP_SERVICE_URI_RU_DEV: String by overriding() -val IDP_SERVICE_URI_TR: String by overriding() -val ERP_API_KEY_GOOGLE_PU: String by overriding() -val ERP_API_KEY_GOOGLE_TU: String by overriding() -val ERP_API_KEY_GOOGLE_RU: String by overriding() -val ERP_API_KEY_GOOGLE_TR: String by overriding() -val ERP_API_KEY_HUAWEI_PU: String by overriding() -val ERP_API_KEY_HUAWEI_TU: String by overriding() -val ERP_API_KEY_HUAWEI_RU: String by overriding() -val ERP_API_KEY_HUAWEI_TR: String by overriding() -val ERP_API_KEY_DESKTOP_PU: String by overriding() -val ERP_API_KEY_DESKTOP_TU: String by overriding() -val ERP_API_KEY_DESKTOP_RU: String by overriding() -val INTEGRITY_API_KEY: String by overriding() -val INTEGRITY_VERIFICATION_KEY: String by overriding() -val CLOUD_PROJECT_NUMBER: String by overriding() -val DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE: String by overriding() -val DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY: String by overriding() + +// versioning +val VERSION_NAME: String by overrides() +val VERSION_CODE: String by overrides() +val USER_AGENT: String by overrides() + +// client id +val CLIENT_ID_RU: String by overrides() +val CLIENT_ID_TU: String by overrides() +val CLIENT_ID_PU: String by overrides() + +// data protection +val DATA_PROTECTION_LAST_UPDATED: String by overrides() + +// OCSP +val VAU_OCSP_RESPONSE_MAX_AGE: String by overrides() + +// trust anchor +val APP_TRUST_ANCHOR_BASE64: String by overrides() +val APP_TRUST_ANCHOR_BASE64_TEST: String by overrides() + +// pharmacy +val PHARMACY_SERVICE_URI: String by overrides() +val PHARMACY_SERVICE_URI_TEST: String by overrides() +val PHARMACY_API_KEY: String by overrides() +val PHARMACY_API_KEY_TEST: String by overrides() + +// organ donation +val ORGAN_DONATION_REGISTER_RU : String by overrides() +val ORGAN_DONATION_REGISTER_PU : String by overrides() +val ORGAN_DONATION_INFO : String by overrides() + +// base service URIs +val BASE_SERVICE_URI_PU: String by overrides() +val BASE_SERVICE_URI_TU: String by overrides() +val BASE_SERVICE_URI_RU: String by overrides() +val BASE_SERVICE_URI_RU_DEV: String by overrides() +val BASE_SERVICE_URI_TR: String by overrides() + +// IDP URIs +val IDP_SERVICE_URI_PU: String by overrides() +val IDP_SERVICE_URI_TU: String by overrides() +val IDP_SERVICE_URI_RU: String by overrides() +val IDP_SERVICE_URI_RU_DEV: String by overrides() +val IDP_SERVICE_URI_TR: String by overrides() + +// ERP API keys google +val ERP_API_KEY_GOOGLE_PU: String by overrides() +val ERP_API_KEY_GOOGLE_TU: String by overrides() +val ERP_API_KEY_GOOGLE_RU: String by overrides() +val ERP_API_KEY_GOOGLE_TR: String by overrides() + +// ERP API keys huawei +val ERP_API_KEY_HUAWEI_PU: String by overrides() +val ERP_API_KEY_HUAWEI_TU: String by overrides() +val ERP_API_KEY_HUAWEI_RU: String by overrides() +val ERP_API_KEY_HUAWEI_TR: String by overrides() + +// ERP API keys desktop +val ERP_API_KEY_DESKTOP_PU: String by overrides() +val ERP_API_KEY_DESKTOP_TU: String by overrides() +val ERP_API_KEY_DESKTOP_RU: String by overrides() + +// integrity +val INTEGRITY_API_KEY: String by overrides() +val INTEGRITY_VERIFICATION_KEY: String by overrides() + +// cloud project number +val CLOUD_PROJECT_NUMBER: String by overrides() + +// virtual health card +val DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE: String by overrides() +val DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY: String by overrides() + +// debug +val DEBUG_TEST_IDS_ENABLED: String by overrides() val DEBUG_VISUAL_TEST_TAGS: String? by project -val APP_CENTER_SECRET: String by overriding() -val BUILD_TYPE_MINIFIED_DEBUG: String by overriding() +val BUILD_TYPE_MINIFIED_DEBUG: String by overrides() + +// app center +val APP_CENTER_SECRET: String by overrides() kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - } - } - } - jvm("desktop") sourceSets { val commonMain by getting { dependencies { - implementation(kotlin("reflect")) - inject { - androidX { - implementation(multiplatformPaging) { - // remove coroutine dependency; otherwise intellij will be confused with "duplicated class import" - exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") - } - } - coroutines { - implementation(coroutinesCore) - } - dateTime { - implementation(datetime) - } - database { - implementation(realm) - } - crypto { - implementation(jose4j) - compileOnly(bouncycastleBcprov) - compileOnly(bouncycastleBcpkix) - } - serialization { - implementation(kotlinXJson) - } - logging { - implementation(napier) - } - network { - implementation(retrofit) - implementation(okhttp3) - implementation(retrofit2KotlinXSerialization) - implementation(okhttpLogging) - } - dependencyInjection { - implementation(kodeinCompose) - implementation(kodein) - } - } implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) @@ -129,103 +117,35 @@ kotlin { implementation(compose.ui) } } - val commonTest by getting { - dependencies { - inject { - database { - implementation(realm) - } - coroutinesTest { - implementation(coroutinesTest) - } - serialization { - implementation(kotlinXJson) - } - test { - implementation(kotlinTest) - implementation(kotlinTestCommon) - implementation(kotlinReflect) - implementation(junit4) - implementation(mockkOld) - implementation(snakeyaml) - } - crypto { - implementation(jose4j) - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - } - networkTest { - implementation(mockWebServer) - } - dateTime { - implementation(datetime) - } - } - } - } val androidMain by getting { dependsOn(commonMain) dependencies { - inject { - androidX { - implementation(coreKtx) - } - crypto { - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - } - dependencyInjection { - implementation(kodeinViewModel) - implementation(kodeinSavedState) - } - } + implementation(libs.androidx.core.ktx) + implementation(libs.bundles.crypto) + implementation(libs.bundles.di.viewmodel) } } val desktopMain by getting { dependsOn(commonMain) dependencies { + implementation(libs.bundles.crypto) implementation(compose.preview) } } - val desktopTest by getting { - dependencies { - inject { - crypto { - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - } - } - } - } } } android { - buildToolsVersion = "33.0.1" - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - compileSdk = Dependencies.Versions.SdkVersions.COMPILE_SDK_VERSION - defaultConfig { - minSdk = Dependencies.Versions.SdkVersions.MIN_SDK_VERSION - } - compileOptions { - sourceCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION - targetCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION - } - buildTypes { - val debug by getting { - isJniDebuggable = true - } - create("minifiedDebug") { - initWith(debug) - } - } namespace = "de.gematik.ti.erp.lib" } + enum class Platforms { Google, Huawei, Konnektathon, Desktop } + enum class Environments { - PU, TU, RU, DEVRU, TR + PU, TU, RU, DEVRU, TR, NONE } + enum class Types { Internal, External } @@ -238,8 +158,16 @@ buildkonfig { buildConfigField(STRING, "INTEGRITY_API_KEY", INTEGRITY_API_KEY) buildConfigField(STRING, "INTEGRITY_VERIFICATION_KEY", INTEGRITY_VERIFICATION_KEY) buildConfigField(STRING, "CLOUD_PROJECT_NUMBER", CLOUD_PROJECT_NUMBER) - buildConfigField(STRING, "DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE", DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) - buildConfigField(STRING, "DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY", DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY) + buildConfigField( + STRING, + "DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE", + DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE + ) + buildConfigField( + STRING, + "DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY", + DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY + ) buildConfigField(STRING, "BUILD_FLAVOR", project.property("buildkonfig.flavor") as String) buildConfigField(STRING, "APP_CENTER_SECRET", APP_CENTER_SECRET) buildConfigField(STRING, "BUILD_TYPE_MINIFIED_DEBUG", BUILD_TYPE_MINIFIED_DEBUG) @@ -253,12 +181,17 @@ buildkonfig { pharmacyServiceUri: String, pharmacyServiceApiKey: String, trustAnchor: String, + clientId: String, ocspResponseMaxAge: String ) { defaultConfigs(flavor) { buildConfigField(STRING, "VERSION_NAME", VERSION_NAME) buildConfigField(INT, "VERSION_CODE", VERSION_CODE) buildConfigField(BOOLEAN, "INTERNAL", isInternal.toString()) + // client ids + buildConfigField(STRING, "CLIENT_ID_TU", CLIENT_ID_TU) + buildConfigField(STRING, "CLIENT_ID_PU", CLIENT_ID_PU) + buildConfigField(STRING, "CLIENT_ID_RU", CLIENT_ID_RU) if (isInternal) { buildConfigField(STRING, "BASE_SERVICE_URI_PU", BASE_SERVICE_URI_PU) buildConfigField(STRING, "BASE_SERVICE_URI_RU", BASE_SERVICE_URI_RU) @@ -282,6 +215,9 @@ buildkonfig { buildConfigField(STRING, "APP_TRUST_ANCHOR_BASE64_TU", APP_TRUST_ANCHOR_BASE64_TEST) buildConfigField(STRING, "IDP_SCOPE_DEVRU", "e-rezept-dev openid") } + buildConfigField(STRING, "ORGAN_DONATION_REGISTER_RU", ORGAN_DONATION_REGISTER_RU) + buildConfigField(STRING, "ORGAN_DONATION_REGISTER_PU", ORGAN_DONATION_REGISTER_PU) + buildConfigField(STRING, "ORGAN_DONATION_INFO", ORGAN_DONATION_INFO) buildConfigField(STRING, "BASE_SERVICE_URI", baseServiceUri) buildConfigField(STRING, "IDP_SERVICE_URI", idpServiceUri) buildConfigField(STRING, "ERP_API_KEY", erpApiKey) @@ -289,6 +225,7 @@ buildkonfig { buildConfigField(STRING, "PHARMACY_API_KEY", pharmacyServiceApiKey) buildConfigField(STRING, "APP_TRUST_ANCHOR_BASE64", trustAnchor) buildConfigField(LONG, "VAU_OCSP_RESPONSE_MAX_AGE", ocspResponseMaxAge) + buildConfigField(STRING, "CLIENT_ID", clientId) buildConfigField(BOOLEAN, "TEST_RUN_WITH_TRUSTSTORE_INTEGRATION", "false") buildConfigField(BOOLEAN, "TEST_RUN_WITH_IDP_INTEGRATION", "false") buildConfigField( @@ -302,6 +239,7 @@ buildkonfig { ) } } + val platforms = Platforms.values() val environments = Environments.values() val types = Types.values() @@ -321,6 +259,7 @@ buildkonfig { Environments.RU -> BASE_SERVICE_URI_RU Environments.DEVRU -> BASE_SERVICE_URI_RU_DEV Environments.TR -> BASE_SERVICE_URI_TR + Environments.NONE -> "" }, idpServiceUri = when (environment) { Environments.PU -> IDP_SERVICE_URI_PU @@ -328,28 +267,31 @@ buildkonfig { Environments.RU -> IDP_SERVICE_URI_RU Environments.DEVRU -> IDP_SERVICE_URI_RU_DEV Environments.TR -> IDP_SERVICE_URI_TR + Environments.NONE -> "" }, erpApiKey = when (platform) { Platforms.Google, Platforms.Konnektathon -> when (environment) { Environments.PU -> ERP_API_KEY_GOOGLE_PU Environments.TU -> ERP_API_KEY_GOOGLE_TU - Environments.DEVRU, - Environments.RU -> ERP_API_KEY_GOOGLE_RU + Environments.DEVRU, Environments.RU -> ERP_API_KEY_GOOGLE_RU Environments.TR -> ERP_API_KEY_GOOGLE_TR + Environments.NONE -> "" } + Platforms.Desktop -> when (environment) { Environments.PU -> ERP_API_KEY_DESKTOP_PU Environments.TU -> ERP_API_KEY_DESKTOP_TU - Environments.DEVRU, - Environments.RU -> ERP_API_KEY_DESKTOP_RU Environments.TR -> ERP_API_KEY_GOOGLE_TR + Environments.DEVRU, Environments.RU -> ERP_API_KEY_DESKTOP_RU + Environments.NONE -> "" } + Platforms.Huawei -> when (environment) { Environments.PU -> ERP_API_KEY_HUAWEI_PU Environments.TU -> ERP_API_KEY_HUAWEI_TU - Environments.DEVRU, - Environments.RU -> ERP_API_KEY_HUAWEI_RU + Environments.DEVRU, Environments.RU -> ERP_API_KEY_HUAWEI_RU Environments.TR -> ERP_API_KEY_HUAWEI_TR + Environments.NONE -> "" } }, pharmacyServiceUri = when (environment) { @@ -358,20 +300,27 @@ buildkonfig { Environments.RU, Environments.DEVRU, Environments.TR -> PHARMACY_SERVICE_URI_TEST + Environments.NONE -> "" }, pharmacyServiceApiKey = when (environment) { Environments.PU -> PHARMACY_API_KEY Environments.TU, Environments.RU, - Environments.DEVRU, - Environments.TR -> PHARMACY_API_KEY_TEST + Environments.DEVRU, Environments.TR -> PHARMACY_API_KEY_TEST + Environments.NONE -> "" }, trustAnchor = when (environment) { Environments.PU -> APP_TRUST_ANCHOR_BASE64 Environments.TU, Environments.RU, - Environments.DEVRU, - Environments.TR -> APP_TRUST_ANCHOR_BASE64_TEST + Environments.DEVRU, Environments.TR -> APP_TRUST_ANCHOR_BASE64_TEST + Environments.NONE -> "" + }, + clientId = when (environment) { + Environments.PU -> CLIENT_ID_PU + Environments.TU, Environments.TR -> CLIENT_ID_TU + Environments.RU, Environments.DEVRU -> CLIENT_ID_RU + Environments.NONE -> "" }, ocspResponseMaxAge = VAU_OCSP_RESPONSE_MAX_AGE ) diff --git a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt index 01e3ed6d..a1af118b 100644 --- a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt @@ -1,37 +1,31 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app -import android.os.Build import java.security.SecureRandom + +// A_19179 @Requirement( - "O.Rand_1", - "O.Rand_2", - "O.Rand_3", - "O.Rand_4", + "O.Rand_1#1", sourceSpecification = "BSI-eRp-ePA", rationale = "Generation of random values by secure random generator specified in FIPS 140-2, " + "Security Requirements for Cryptographic Modules, section 4.9.1." ) actual fun secureRandomInstance(): SecureRandom = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - SecureRandom.getInstanceStrong() - } else { - SecureRandom() - } + SecureRandom.getInstanceStrong() diff --git a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt index 948d4666..6456931c 100644 --- a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt index af144cd7..912f374b 100644 --- a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt index 2a9f85be..b9166a87 100644 --- a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt index 6b6ed7ed..88de32e1 100644 --- a/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/BCProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/BCProvider.kt index 8efd98af..fdb4059f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/BCProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/BCProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/CryptoUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/CryptoUtils.kt index 01a613a1..c6a6bb83 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/CryptoUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/CryptoUtils.kt @@ -1,42 +1,41 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app import javax.crypto.KeyGenerator import javax.crypto.SecretKey @Requirement( - "A_19179", - "A_21323", - sourceSpecification = "gemSpec_eRp_FdV", + "A_21323#1", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Entropy is ensured by using SecureRandom for generation." - // TODO Update this req. when using the health card for random number generation is also implemented for Android. ) @Requirement( - "GS-A_4368", + "GS-A_4368#1", "GS-A_4367#1", sourceSpecification = "gemSpec_Krypt", rationale = "Entropy is ensured by using SecureRandom for generation. The only statement regarding the quality " + "of random number generation from Android is, that the requirements of FIPS 140-2 are met." + "However, there is no direct relation between FIPS 140-2 and DRG.2, because DRG.2 describes a concrete " + "implementation of a PRNG, and FIPS 140-2 defines requirements on the quality of randomness." - // TODO Update this req. when using the health card for random number generation is also implemented for Android. ) @Requirement( "O.Cryp_3#1", diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt index 5142e90e..877d0a89 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/Requirement.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/Requirement.kt index 13bd3c7f..850c9980 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/Requirement.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/Requirement.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app @Target( @@ -39,5 +40,6 @@ package de.gematik.ti.erp.app annotation class Requirement( vararg val requirements: String, val sourceSpecification: String, - val rationale: String + val rationale: String, + val codeLines: Int = 15 ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt index 2d52c34f..96d853cb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt index a5de5700..4afe6b17 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt @@ -1,28 +1,34 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.usecase +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.settings.repository.SettingsRepository import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +@Requirement( + "O.Purp_5#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "allow/disallow analytics state useCase" +) class ChangeAnalyticsStateUseCase( private val repository: SettingsRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt index 60e94a3e..c9d8b09b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.analytics.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt index be0fddb4..c1fbbbeb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.serialization.json.JsonElement import retrofit2.Response @@ -30,6 +31,11 @@ import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.Tag +@Requirement( + "O.Purp_8#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Interface of Erp(Fachdienst)-service" +) interface ErpService { @GET("Task/{id}") @@ -70,7 +76,7 @@ interface ErpService { suspend fun postCommunication( @Tag profileId: ProfileIdentifier, @Body communication: JsonElement, - @Header("X-AccessCode") accessCode: String? = null + @Header("X-AccessCode") accessCode: String ): Response @GET("Communication") @@ -88,6 +94,12 @@ interface ErpService { @Query("identifier") id: String ): Response + @GET("Medication") + suspend fun getMedication( + @Tag profileId: ProfileIdentifier, + @Query("reference") id: String + ): Response + // PKV consent @GET("Consent") suspend fun getConsent( diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpServiceState.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpServiceState.kt new file mode 100644 index 00000000..faa9d9ec --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpServiceState.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.api + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.Requirement + +interface ErpServiceState + +@Requirement( + "O.Source_3#1", + "O.Source_4#1", + "O.Plat_4#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Error messages are localized using the `ErpServiceErrorState " + + "Search for `ErpServiceErrorState`or State.Error to see all instances." + + "Most errors are localized with static text. Logging is only active on debug builds." +) +@Requirement( + "A_19937#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Local mapping of errors" +) +@Deprecated("Use HttpErrorState instead") +interface ErpServiceErrorState : ErpServiceState + +@Deprecated("Use HttpErrorState instead") +@Stable +sealed interface GeneralErrorState : ErpServiceErrorState { + data object NetworkNotAvailable : GeneralErrorState + class ServerCommunicationFailedWhileRefreshing(val code: Int) : GeneralErrorState + data object FatalTruststoreState : GeneralErrorState + data object NoneEnrolled : GeneralErrorState + data object UserNotAuthenticated : GeneralErrorState + data object RedirectUrlForExternalAuthenticationWrong : GeneralErrorState +} + +@Immutable +data class RefreshedState(val nrOfNewPrescriptions: Int) : ErpServiceState diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpErrorState.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpErrorState.kt new file mode 100644 index 00000000..19a75ed7 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpErrorState.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.api + +import de.gematik.ti.erp.app.Requirement +import io.github.aakira.napier.Napier +import retrofit2.Response +import java.net.HttpURLConnection +import java.net.UnknownHostException + +//region sources +// https://github.com/gematik/api-erp/blob/master/docs/erp_chargeItem.adoc#anwendungsfall-abrechnungsinformation-zum-%C3%A4ndern-abrufen +// https://github.com/gematik/api-erp/blob/master/docs/erp_communication.adoc +// https://github.com/gematik/api-erp/blob/master/docs/erp_consent.adoc +//endregion + +@Requirement( + "O.Source_3#1", + "O.Source_4#1", + "O.Plat_4#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Error messages are localized using the `HttpErrorState " + + "Search for `HttpErrorState` to see all instances." + + "Most errors are localized with static text. Logging is only active on debug builds." +) +@Requirement( + "A_19937#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Mapping of errors" +) +@Suppress("MagicNumber") +sealed class HttpErrorState(val errorCode: Int) { + data object Unknown : HttpErrorState(-1) + data object BadRequest : HttpErrorState(HttpURLConnection.HTTP_BAD_REQUEST) + data object Unauthorized : HttpErrorState(HttpURLConnection.HTTP_UNAUTHORIZED) + data object Forbidden : HttpErrorState(HttpURLConnection.HTTP_FORBIDDEN) + data object NotFound : HttpErrorState(HttpURLConnection.HTTP_NOT_FOUND) + data object MethodNotAllowed : HttpErrorState(HttpURLConnection.HTTP_BAD_METHOD) + data object RequestTimeout : HttpErrorState(HttpURLConnection.HTTP_CLIENT_TIMEOUT) + data object Conflict : HttpErrorState(HttpURLConnection.HTTP_CONFLICT) // not present in docs + data object Gone : HttpErrorState(HttpURLConnection.HTTP_GONE) + data object TooManyRequest : HttpErrorState(429) + data object ServerError : HttpErrorState(HttpURLConnection.HTTP_INTERNAL_ERROR) + data class ErrorWithCause(val message: String) : HttpErrorState(-1) +} + +// TODO: Use code from OperationOutcome +/* + { + "resourceType":"OperationOutcome", + "meta":{ + "profile":["http://hl7.org/fhir/StructureDefinition/OperationOutcome"] + }, + "issue":[ + { + "severity":"error", + "code":"invalid", + "details":{"text":"Referenced task does not contain a KVNR"} + } + ] + } + */ +@Suppress("MagicNumber") +fun Response<*>.httpErrorState(): HttpErrorState { + Napier.e { "http error code ${code()}" } + try { + return when (this.code()) { + HttpURLConnection.HTTP_BAD_REQUEST -> HttpErrorState.BadRequest // 400 + HttpURLConnection.HTTP_UNAUTHORIZED -> HttpErrorState.Unauthorized // 401 + HttpURLConnection.HTTP_FORBIDDEN -> HttpErrorState.Forbidden // 403 + HttpURLConnection.HTTP_NOT_FOUND -> HttpErrorState.NotFound // 404 + HttpURLConnection.HTTP_BAD_METHOD -> HttpErrorState.MethodNotAllowed // 405 + HttpURLConnection.HTTP_CLIENT_TIMEOUT -> HttpErrorState.RequestTimeout // 408 + HttpURLConnection.HTTP_CONFLICT -> HttpErrorState.Conflict // 409 + HttpURLConnection.HTTP_GONE -> HttpErrorState.Gone // 410 + 429 -> HttpErrorState.TooManyRequest // 429 + HttpURLConnection.HTTP_INTERNAL_ERROR -> HttpErrorState.ServerError // 500 + else -> { + HttpErrorState.ErrorWithCause("Unknown error ${this.code()}") + } + } + } catch (error: Throwable) { + if (error.cause?.cause is UnknownHostException) { + Napier.e { "Error on no internet" } + } else { + Napier.e("Unknown error on http call", error) + } + return HttpErrorState.Unknown + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpStatusCodes.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpStatusCodes.kt index 6695612d..e5872c4c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpStatusCodes.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/HttpStatusCodes.kt @@ -1,23 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api +import de.gematik.ti.erp.app.Requirement + +@Requirement( + "A_19937#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Server error codes are defined in the HTTP standard." +) // Informational responses (100-199) const val HTTP_CONTINUE = 100 const val HTTP_SWITCHING_PROTOCOLS = 101 diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/NetworkUtil.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/NetworkUtil.kt index 56fbad08..dfd8845a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/NetworkUtil.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/NetworkUtil.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api @@ -22,9 +22,14 @@ import io.github.aakira.napier.Napier import kotlinx.coroutines.CancellationException import retrofit2.Response import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException class ApiCallException(message: String, val response: Response<*>) : IOException(message) +data class NoInternetException(override val message: String? = null, val exception: Exception? = null) : IOException(message) + /** * Wraps a remote call in a try catch and returns [Result.Error] with an [IOException] in case [call] couldn't be executed. * In case of a successful response, the [Result.Success] contains the body of it. @@ -46,10 +51,21 @@ suspend fun safeApiCall( throw e } catch (e: Exception) { Napier.e("Api Call Error", e) + e.mapToResultFailure(errorMessage) + } + +private fun Exception.mapToResultFailure(errorMessage: String): Result = + when { + isNetworkException(this) -> Result.failure(NoInternetException(errorMessage, this)) + // An exception was thrown when calling the API so we're converting this to an [IOException] - Result.failure(IOException(errorMessage, e)) + else -> Result.failure(IOException(errorMessage, this)) } +private fun isNetworkException(e: Exception): Boolean { + return e is UnknownHostException || e is ConnectException || e is SocketTimeoutException +} + /** * This safeApi call should only be used if it's necessary to do all error handling on its own apart from an io exception. */ @@ -62,11 +78,10 @@ suspend fun safeApiCallRaw( } catch (e: Exception) { Napier.e("Api Call Error", e) // An exception was thrown when calling the API so we're converting this to an IOException - Result.failure(IOException(errorMessage, e)) + e.mapToResultFailure(errorMessage) } suspend fun safeApiCallNullable( - errorMessage: String, call: suspend () -> Response ): Result = try { @@ -79,6 +94,5 @@ suspend fun safeApiCallNullable( ) } } catch (e: Exception) { - // An exception was thrown when calling the API so we're converting this to an [IOException] - Result.failure(IOException(errorMessage, e)) + Result.failure(e) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacyRedeemService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacyRedeemService.kt index 96db1cce..09bd54cf 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacyRedeemService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacyRedeemService.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api +import de.gematik.ti.erp.app.Requirement import okhttp3.RequestBody import retrofit2.Response import retrofit2.http.Body @@ -25,6 +26,11 @@ import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Url +@Requirement( + "O.Purp_8#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Interface of pharmacy redeem service" +) interface PharmacyRedeemService { @POST diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacySearchService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacySearchService.kt index 58066bf6..c4905a38 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacySearchService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/PharmacySearchService.kt @@ -1,29 +1,35 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api +import de.gematik.ti.erp.app.Requirement import kotlinx.serialization.json.JsonElement import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query import retrofit2.http.QueryMap +@Requirement( + "O.Purp_8#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Interface of pharmacy search service" +) interface PharmacySearchService { @GET("api/Location") diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt index 487917d0..0e9b24f5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api @@ -142,7 +142,6 @@ abstract class ResourcePaging( ): Result> protected abstract suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? - companion object { fun Instant?.toTimestampString() = this?.let { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/appupdate/usecase/CheckVersionUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/appupdate/usecase/CheckVersionUseCase.kt index d7aa7c75..57ce8d5e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/appupdate/usecase/CheckVersionUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/appupdate/usecase/CheckVersionUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.appupdate.usecase @@ -30,10 +30,9 @@ import java.io.IOException import java.net.HttpURLConnection @Requirement( - "O.Arch_10#1", + "O.Arch_10#3", sourceSpecification = "BSI-eRp-ePA", - rationale = "Currently only implemented via APIKey usage against the FD. " + - "All requests against the backends may respond with an 403 status code." + rationale = "Business logic to make the check for an update using APOVZD and FD." ) class CheckVersionUseCase( private val okHttp: OkHttpClient, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt index 72af9848..03ec4034 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.authentication.mapper diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt index d271f87d..242d7326 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.authentication.model @@ -29,7 +29,7 @@ sealed interface InitialAuthenticationData { data class HealthCard(val can: String, override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData -data class SecureElement(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData +data class Biometric(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData data class External( val authenticatorId: String, val authenticatorName: String, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt index 708356ac..a84d2803 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.authentication.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/CardUtilities.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/CardUtilities.kt index 9351dca1..8dee8724 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/CardUtilities.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/CardUtilities.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/CardKey.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/CardKey.kt index 15d821e4..47980a09 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/CardKey.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/CardKey.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card @@ -24,6 +24,7 @@ private const val MAX_KEY_ID = 28 /** * Class applies for symmetric keys and private keys. */ +@Suppress("ImplicitDefaultLocale") class CardKey(private val keyId: Int) : ICardKeyReference { init { require(!(keyId < MIN_KEY_ID || keyId > MAX_KEY_ID)) { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/EncryptedPinFormat2.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/EncryptedPinFormat2.kt index 87919aa8..164f8b33 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/EncryptedPinFormat2.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/EncryptedPinFormat2.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/HealthCardVersion2.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/HealthCardVersion2.kt index c7cbb156..57ff5f9c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/HealthCardVersion2.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/HealthCardVersion2.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.card import org.bouncycastle.asn1.ASN1InputStream diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardChannel.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardChannel.kt index 3a1b6329..7ab905b6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardChannel.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardChannel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardKeyReference.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardKeyReference.kt index d339be21..3ff5eada 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardKeyReference.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/ICardKeyReference.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/IHealthCard.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/IHealthCard.kt index e529b21e..4af42c8c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/IHealthCard.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/IHealthCard.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PaceKey.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PaceKey.kt index e3bea420..34a9f327 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PaceKey.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PaceKey.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PasswordReference.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PasswordReference.kt index b281b247..8ae8f286 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PasswordReference.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PasswordReference.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PsoAlgorithm.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PsoAlgorithm.kt index 68a8912e..3c9da8bc 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PsoAlgorithm.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/PsoAlgorithm.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.card diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/SecureMessaging.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/SecureMessaging.kt index 7284fcbd..f24a59bf 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/SecureMessaging.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/card/SecureMessaging.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.card import de.gematik.ti.erp.app.BCProvider diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/cardobjects/FileSystem.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/cardobjects/FileSystem.kt index 3399f66e..7d8a15f6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/cardobjects/FileSystem.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/cardobjects/FileSystem.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.cardobjects diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/Apdu.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/Apdu.kt index f1104606..e37b2323 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/Apdu.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/Apdu.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.command import java.io.ByteArrayOutputStream @@ -84,6 +86,7 @@ class CommandApdu( ne: Int? ) = ofOptions(cla = cla, ins = ins, p1 = p1, p2 = p2, data = null, ne = ne) + @Suppress("CyclomaticComplexMethod") fun ofOptions( cla: Int, ins: Int, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ChangeReferenceDataCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ChangeReferenceDataCommand.kt index 3d2ba454..56b11dfb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ChangeReferenceDataCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ChangeReferenceDataCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GeneralAuthenticateCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GeneralAuthenticateCommand.kt index 436b57b1..f0f55f91 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GeneralAuthenticateCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GeneralAuthenticateCommand.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.command import org.bouncycastle.asn1.BERTags diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetPinStatusCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetPinStatusCommand.kt index d6dc0b07..adf44d6a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetPinStatusCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetPinStatusCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetRandomValues.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetRandomValues.kt index 5869d380..0e23b3b7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetRandomValues.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/GetRandomValues.kt @@ -1,25 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command -import de.gematik.ti.erp.app.Requirement - /** * Commands representing Get Random command in gemSpec_COS#14.9.5 */ @@ -31,13 +29,6 @@ private const val NO_MEANING = 0x00 /** * Use case Get Random gemSpec_COS#14.9.5.1 */ -@Requirement( - "GS-A_4367#6", - "GS-A_4368#5", - sourceSpecification = "gemSpec_Krypt", - rationale = "Random numbers are generated using the RNG of the health card." + - "This generator fulfills BSI-TR-03116#3.4 PTG.2 required by gemSpec_COS#14.9.5.1" -) fun HealthCardCommand.Companion.getRandomValues( length: Int ) = diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/HealthCardCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/HealthCardCommand.kt index 8f67eb41..ff503f0e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/HealthCardCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/HealthCardCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ManageSecurityEnvironmentCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ManageSecurityEnvironmentCommand.kt index fbc017ea..c16ccced 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ManageSecurityEnvironmentCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ManageSecurityEnvironmentCommand.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.command import de.gematik.ti.erp.app.card.model.card.CardKey diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/PsoComputeDigitalSignatureCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/PsoComputeDigitalSignatureCommand.kt index 9ceba18d..dd10a579 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/PsoComputeDigitalSignatureCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/PsoComputeDigitalSignatureCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ReadCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ReadCommand.kt index 2fa22244..d81c54f2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ReadCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ReadCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ResponseStatus.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ResponseStatus.kt index b0fb9277..001fd5de 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ResponseStatus.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/ResponseStatus.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/SelectCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/SelectCommand.kt index 8f9bc5cc..9b3f5fce 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/SelectCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/SelectCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/UnlockEgkCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/UnlockEgkCommand.kt index a01212ee..8cbf2277 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/UnlockEgkCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/UnlockEgkCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/VerifyCommand.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/VerifyCommand.kt index f4526fe0..3116df46 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/VerifyCommand.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/command/VerifyCommand.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.command diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/CertificateExchange.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/CertificateExchange.kt index 15061a2a..8d5d4844 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/CertificateExchange.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/CertificateExchange.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/KeyDerivationFunction.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/KeyDerivationFunction.kt index c3ef98dd..14f6074f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/KeyDerivationFunction.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/KeyDerivationFunction.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PaceInfo.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PaceInfo.kt index 78ceab96..3f4d626d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PaceInfo.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PaceInfo.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.card.model.CardUtilities import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.ASN1Integer @@ -57,6 +58,12 @@ class PaceInfo(cardAccess: ByteArray) { */ val protocolID: String = protocol.id + @Requirement( + "O.Cryp_5#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Brainpool curves P256 used", + codeLines = 4 + ) private val ecNamedCurveParameterSpec = ECNamedCurveTable.getParameterSpec( when (parameterID) { PARAMETER256 -> "BrainpoolP256r1" diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PinExchange.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PinExchange.kt index 6272e3e9..33439d3d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PinExchange.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/PinExchange.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/RandomExchange.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/RandomExchange.kt index 809754ca..80f8a39a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/RandomExchange.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/RandomExchange.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange -import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.card.model.card.ICardChannel import de.gematik.ti.erp.app.card.model.command.HealthCardCommand import de.gematik.ti.erp.app.card.model.command.ResponseStatus @@ -26,13 +25,6 @@ import de.gematik.ti.erp.app.card.model.command.executeSuccessfulOn import de.gematik.ti.erp.app.card.model.command.getRandomValues import de.gematik.ti.erp.app.card.model.command.select -@Requirement( - "GS-A_4367#5", - "GS-A_4368#4", - sourceSpecification = "gemSpec_Krypt", - rationale = "Random numbers are generated using the RNG of the health card." + - "This generator fulfills BSI-TR-03116#3.4 PTG.2 required by gemSpec_COS#14.9.5.1" -) fun ICardChannel.getRandom(length: Int): ByteArray { HealthCardCommand.select(selectParentElseRoot = false, readFirst = false) .executeSuccessfulOn(this) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/SignChallengeExchange.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/SignChallengeExchange.kt index db9ec115..dfa55b6e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/SignChallengeExchange.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/SignChallengeExchange.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.exchange @@ -32,20 +32,19 @@ import de.gematik.ti.erp.app.card.model.command.select import de.gematik.ti.erp.app.card.model.identifier.ApplicationIdentifier @Requirement( - "A_20526-01", - "A_17205", - "A_17207", - "A_17359", - "A_20172#3", - "A_20700-07#1", - sourceSpecification = "gemF_Tokenverschlüsselung", - rationale = "Sign challenge using the health card certificate." + "O.Cryp_1#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Signature via ecdh ephemeral-static [one time usage]" +) +@Requirement( + "A_17207#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "Signature via ecdh ephemeral-static [one time usage]" ) @Requirement( - "O.Cryp_1#1", "O.Cryp_4#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature creation with eGK with dedicated C.CH.AUT" ) fun ICardChannel.signChallenge(challenge: ByteArray): ByteArray { HealthCardCommand.select(ApplicationIdentifier(Df.Esign.AID)).executeSuccessfulOn(this) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/TrustedChannelPaceKeyExchange.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/TrustedChannelPaceKeyExchange.kt index e38ba0a3..8d99c480 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/TrustedChannelPaceKeyExchange.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/exchange/TrustedChannelPaceKeyExchange.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.cardwall.model.nfc.exchange import de.gematik.ti.erp.app.Requirement @@ -105,15 +107,19 @@ suspend fun ICardChannel.establishTrustedChannel(cardAccessNumber: String): Pace @Requirement( "O.Cryp_3#2", - "O.Cryp_4#2", sourceSpecification = "BSI-eRp-ePA", rationale = "AES Key-Generation and one time usage" ) + @Requirement( + "O.Cryp_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "One time usage for JWE ECDH-ES Encryption" + ) val aes128Key = getAES128Key(canBytes, KeyDerivationFunction.Mode.PASSWORD) val encKey = KeyParameter(aes128Key) val nonceS = ByteArray(AES_BLOCK_SIZE) - AESEngine().apply { + AESEngine.newInstance().apply { init(false, encKey) processBlock(nonceZBytesEncoded, 0, nonceS, 0) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ApplicationIdentifier.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ApplicationIdentifier.kt index b05fc64a..0dcdf1d5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ApplicationIdentifier.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ApplicationIdentifier.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("ImplicitDefaultLocale") + package de.gematik.ti.erp.app.card.model.identifier import org.bouncycastle.util.encoders.Hex diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/FileIdentifier.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/FileIdentifier.kt index c466de94..9dc77459 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/FileIdentifier.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/FileIdentifier.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.card.model.identifier import org.bouncycastle.util.encoders.Hex diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ShortFileIdentifier.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ShortFileIdentifier.kt index d2a9304e..77c0c6c9 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ShortFileIdentifier.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/identifier/ShortFileIdentifier.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.card.model.identifier @@ -37,6 +37,7 @@ class ShortFileIdentifier(val sfId: Int) { constructor(hexSfId: String) : this(Hex.decode(hexSfId)[0].toInt()) + @Suppress("ImplicitDefaultLocale") private fun sanityCheck() { require(!(sfId < MIN_VALUE || sfId > MAX_VALUE)) { // gemSpec_COS#N007.000 diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/DataObject.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/DataObject.kt index ad7c00c2..5cc9e761 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/DataObject.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/DataObject.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.tagobjects diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/LengthObject.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/LengthObject.kt index d0fd446d..da710b93 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/LengthObject.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/LengthObject.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.tagobjects diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/MacObject.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/MacObject.kt index baee6381..25a62927 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/MacObject.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/MacObject.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.tagobjects diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/StatusObject.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/StatusObject.kt index 78c8bad5..c69948c8 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/StatusObject.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/card.model/tagobjects/StatusObject.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.cardwall.model.nfc.tagobjects diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/DefaultInAppMessageRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/DefaultInAppMessageRepository.kt new file mode 100644 index 00000000..98e2a794 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/DefaultInAppMessageRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.changelogs + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import io.realm.kotlin.types.RealmList +import kotlinx.coroutines.flow.Flow + +class DefaultInAppMessageRepository( + private val inAppDataSource: InAppDataSource +) : InAppMessageRepository { + override val inAppMessages: Flow> = + inAppDataSource.changeLogs + + override val counter: Flow = + inAppDataSource.counter + + override val lastVersion: Flow = + inAppDataSource.lastVersion + + override val lastUpdatedVersion: Flow = + inAppDataSource.lastUpdatedVersion + + override val showWelcomeMessage: Flow = + inAppDataSource.showWelcomeMessage + + override suspend fun setInternalMessageAsRead() = + inAppDataSource.setInternalMessageAsRead() + + override suspend fun setShowWelcomeMessage() = + inAppDataSource.setShowWelcomeMessage() + + override suspend fun updateChangeLogs(newChangeLogs: RealmList, lastVersion: String, inAppLastVersion: String) = + inAppDataSource.updateChangeLogs(newChangeLogs, lastVersion, inAppLastVersion) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppDataSource.kt new file mode 100644 index 00000000..988af919 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppDataSource.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.changelogs + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InternalMessageEntity +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.writeOrCopyToRealm +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.types.RealmList +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext + +class InAppDataSource( + private val realm: Realm, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + val changeLogs: Flow> + get() = realm.query().asFlow().mapNotNull { + it.list.firstOrNull()?.inAppMessageEntity + }.flowOn(dispatchers) + + val counter: Flow + get() = realm.query().asFlow().mapNotNull { + it.list.firstOrNull()?.counter + } + + val lastUpdatedVersion: Flow + get() = realm.query() + .asFlow() + .map { it.list.firstOrNull()?.lastUpdatedVersion } + + val lastVersion: Flow + get() = realm.query() + .asFlow() + .map { it.list.firstOrNull()?.lastVersion } + + val showWelcomeMessage: Flow + get() = realm.query() + .asFlow() + .mapNotNull { + it.list.firstOrNull()?.showWelcomeMessage ?: false + } + + suspend fun setInternalMessageAsRead() { + withContext(dispatchers) { + realm.writeOrCopyToRealm(::InternalMessageEntity) { entity -> + entity.inAppMessageEntity.forEach { + it.isUnRead = false + } + entity.counter = 0 + } + } + } + + suspend fun setShowWelcomeMessage() { + withContext(dispatchers) { + realm.writeOrCopyToRealm(::InternalMessageEntity) { entity -> + entity.showWelcomeMessage = true + } + } + } + + suspend fun updateChangeLogs(newChangeLogs: RealmList, lastVersion: String, inAppLastVersion: String) { + withContext(dispatchers) { + realm.writeBlocking { + queryFirst()?.apply { + newChangeLogs.forEach { message -> + this.inAppMessageEntity = this.inAppMessageEntity.plus(message).toRealmList() + this.counter = this.inAppMessageEntity.filter { it.isUnRead }.size.toLong() + if (this.lastVersion.isNullOrBlank()) { + this.lastVersion = lastVersion + } + this.lastUpdatedVersion = inAppLastVersion + } + } ?: run { + copyToRealm( + InternalMessageEntity().apply { + this.inAppMessageEntity = newChangeLogs + this.counter = this.inAppMessageEntity.filter { it.isUnRead }.size.toLong() + if (this.lastVersion.isNullOrBlank()) { + this.lastVersion = lastVersion + } + this.lastUpdatedVersion = inAppLastVersion + } + ) + } + } + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppMessageRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppMessageRepository.kt new file mode 100644 index 00000000..15fb7af2 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/changelogs/InAppMessageRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.changelogs + +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import io.realm.kotlin.types.RealmList +import kotlinx.coroutines.flow.Flow + +interface InAppMessageRepository { + + val inAppMessages: Flow> + + val counter: Flow + + val lastVersion: Flow + + val lastUpdatedVersion: Flow + + val showWelcomeMessage: Flow + + suspend fun setInternalMessageAsRead() + + suspend fun setShowWelcomeMessage() + + suspend fun updateChangeLogs(newChangeLogs: RealmList, lastVersion: String, inAppLastVersion: String) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt index 6d01c7d6..ed7ae5d6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.model @@ -59,7 +59,7 @@ private fun template( ], "patient": { "identifier": { - "system": "http://fhir.de/NamingSystem/gkv/kvid-10", + "system": "http://fhir.de/sid/pkv/kvid-10", "value": "$insuranceIdentifier" } }, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConstentState.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConstentState.kt new file mode 100644 index 00000000..42197915 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConstentState.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.model + +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.api.HTTP_BAD_REQUEST +import de.gematik.ti.erp.app.api.HTTP_CONFLICT +import de.gematik.ti.erp.app.api.HTTP_FORBIDDEN +import de.gematik.ti.erp.app.api.HTTP_INTERNAL_ERROR +import de.gematik.ti.erp.app.api.HTTP_METHOD_NOT_ALLOWED +import de.gematik.ti.erp.app.api.HTTP_NOT_FOUND +import de.gematik.ti.erp.app.api.HTTP_REQUEST_TIMEOUT +import de.gematik.ti.erp.app.api.HTTP_TOO_MANY_REQUESTS +import de.gematik.ti.erp.app.api.HTTP_UNAUTHORIZED +import io.github.aakira.napier.Napier +import okio.IOException + +enum class ConsentContext { + GetConsent, + GrantConsent, + RevokeConsent +} + +@Stable +sealed interface ConsentState : ErpServiceState { + + @Stable + sealed interface ValidState : ConsentState { + data object UnknownConsent : ConsentState + data object Loading : ConsentState + data object NotGranted : ConsentState + data class Granted(val context: ConsentContext) : ConsentState + data object Revoked : ConsentState + } + + @Stable + sealed interface ConsentErrorState : ConsentState { + data object AlreadyGranted : ConsentErrorState + data object ChargeConsentAlreadyRevoked : ConsentErrorState + data class InternalError(val context: ConsentContext) : ConsentErrorState + data class ServerTimeout(val context: ConsentContext) : ConsentErrorState + data object Unauthorized : ConsentErrorState + data class TooManyRequests(val context: ConsentContext) : ConsentErrorState + data class NoInternet(val context: ConsentContext) : ConsentErrorState + data object BadRequest : ConsentErrorState + data object Forbidden : ConsentErrorState + data object Unknown : ConsentErrorState + } + + companion object { + fun ConsentState.isNotGranted(): Boolean = + (this != ValidState.Granted(ConsentContext.GetConsent) && this != ValidState.Granted(ConsentContext.GrantConsent)) && + (this == ValidState.NotGranted || this == ValidState.Revoked) + + fun ConsentState.isConsentGranted(): Boolean = + this == ValidState.Granted(ConsentContext.GetConsent) || + this == ValidState.Granted(ConsentContext.GrantConsent) || + this == ConsentErrorState.AlreadyGranted + } +} + +// TODO: Use http error states +fun mapConsentErrorStates(error: Throwable, context: ConsentContext): ErpServiceState { + Napier.e { "consent error code ${error.cause?.cause}" } + return when (error) { + is ApiCallException -> { + val errorCode = (error as? ApiCallException)?.response?.code() + when (errorCode) { + HTTP_CONFLICT -> ConsentState.ConsentErrorState.AlreadyGranted + HTTP_REQUEST_TIMEOUT -> ConsentState.ConsentErrorState.ServerTimeout(context) + HTTP_INTERNAL_ERROR -> ConsentState.ConsentErrorState.InternalError(context) + HTTP_TOO_MANY_REQUESTS -> ConsentState.ConsentErrorState.TooManyRequests(context) + HTTP_NOT_FOUND -> ConsentState.ConsentErrorState.ChargeConsentAlreadyRevoked + HTTP_BAD_REQUEST, HTTP_METHOD_NOT_ALLOWED -> ConsentState.ConsentErrorState.BadRequest + HTTP_FORBIDDEN -> ConsentState.ConsentErrorState.Forbidden + HTTP_UNAUTHORIZED -> ConsentState.ConsentErrorState.Unauthorized + else -> ConsentState.ConsentErrorState.Unknown // silent fail + } + } + + is IOException -> ConsentState.ConsentErrorState.Unknown // TODO: We use this because the safeApiCall is wrongly configured. + else -> ConsentState.ConsentErrorState.Unknown + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentLocalDataSource.kt index be501e37..75e826c6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.repository @@ -37,4 +37,8 @@ class ConsentLocalDataSource( fun getConsentDrawerShown(profileIdentifier: ProfileIdentifier): Boolean = realm.queryFirst("id = $0", profileIdentifier) ?.isConsentDrawerShown ?: false + + fun getInsuranceId(profileId: ProfileIdentifier): String? = + realm.queryFirst("id = $0", profileId) + ?.insuranceIdentifier } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt index 33549d87..b670a091 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt index 8d130bd3..d1d90e6f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.repository @@ -40,4 +40,6 @@ interface ConsentRepository { fun isConsentDrawerShown(profileId: ProfileIdentifier): Boolean fun isConsentGranted(it: JsonElement): Boolean + + fun getInsuranceId(profileId: ProfileIdentifier): String? } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/DefaultConsentRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/DefaultConsentRepository.kt index 6a365b54..ef588e78 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/DefaultConsentRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/DefaultConsentRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.repository @@ -58,4 +58,7 @@ class DefaultConsentRepository( } return granted } + + override fun getInsuranceId(profileId: ProfileIdentifier): String? = + localDataSource.getInsuranceId(profileId) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GetConsentUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GetConsentUseCase.kt index 5d396abc..a77f2892 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GetConsentUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GetConsentUseCase.kt @@ -1,37 +1,52 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.usecase +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.mapConsentErrorStates import de.gematik.ti.erp.app.consent.repository.ConsentRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.withContext class GetConsentUseCase( private val consentRepository: ConsentRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - suspend operator fun invoke(profileIdentifier: ProfileIdentifier) = + suspend operator fun invoke(profileIdentifier: ProfileIdentifier): Flow = flowOf( withContext(dispatcher) { - consentRepository.getConsent(profileId = profileIdentifier).map { - consentRepository.isConsentGranted(it) - } + consentRepository.getConsent(profileId = profileIdentifier).fold( + onSuccess = { + when (consentRepository.isConsentGranted(it)) { + true -> ConsentState.ValidState.Granted(ConsentContext.GetConsent) + false -> ConsentState.ValidState.NotGranted + } + }, + onFailure = { + mapConsentErrorStates(it, ConsentContext.GetConsent) + } + ) } + ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GrantConsentUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GrantConsentUseCase.kt index 3ae788a1..694dae3c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GrantConsentUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/GrantConsentUseCase.kt @@ -1,28 +1,34 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.usecase +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState import de.gematik.ti.erp.app.consent.model.createConsent +import de.gematik.ti.erp.app.consent.model.mapConsentErrorStates import de.gematik.ti.erp.app.consent.repository.ConsentRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.withContext class GrantConsentUseCase( @@ -30,14 +36,24 @@ class GrantConsentUseCase( private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend operator fun invoke( - profileIdentifier: ProfileIdentifier, - insuranceId: String - ): Result = + profileId: ProfileIdentifier + ): Flow = flowOf( withContext(dispatcher) { - val consent = createConsent(insuranceId) - repository.grantConsent( - profileId = profileIdentifier, - consent = consent - ) + repository.getInsuranceId(profileId)?.let { id -> + val consent = createConsent(id) + repository.grantConsent( + profileId = profileId, + consent = consent + ).fold( + onSuccess = { + ConsentState.ValidState.Granted(ConsentContext.GrantConsent) + }, + onFailure = { + mapConsentErrorStates(it, ConsentContext.GrantConsent) + } + + ) + } ?: ConsentState.ConsentErrorState.Unknown } + ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/RevokeConsentUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/RevokeConsentUseCase.kt index 4271fa89..c40bc6eb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/RevokeConsentUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/RevokeConsentUseCase.kt @@ -1,28 +1,50 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.usecase +import de.gematik.ti.erp.app.api.ErpServiceState +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.model.mapConsentErrorStates import de.gematik.ti.erp.app.consent.repository.ConsentRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.withContext -class RevokeConsentUseCase(private val consentRepository: ConsentRepository) { - suspend operator fun invoke(profileIdentifier: ProfileIdentifier) = consentRepository.revokeChargeConsent( - profileId = profileIdentifier - ) +class RevokeConsentUseCase( + private val consentRepository: ConsentRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(profileIdentifier: ProfileIdentifier): Flow = + withContext(dispatcher) { + flowOf( + consentRepository.revokeChargeConsent( + profileId = profileIdentifier + ).fold( + onSuccess = { + ConsentState.ValidState.Revoked + }, + onFailure = { mapConsentErrorStates(it, ConsentContext.RevokeConsent) } + ) + ) + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCase.kt index 94ed233f..b1e669b4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentDrawerUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentDrawerUseCase.kt new file mode 100644 index 00000000..d803a685 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentDrawerUseCase.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.usecase + +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ShowGrantConsentDrawerUseCase( + private val consentRepository: ConsentRepository, + private val profilesRepository: ProfileRepository +) { + operator fun invoke(): Flow = + profilesRepository.activeProfile().map { profile -> + with(profile) { + isPkv() && isConsentDrawerNotShown() && + isSsoValid() && isConsentNotGranted() + } + } + + private fun ProfilesData.Profile.isPkv() = insuranceType == ProfilesData.InsuranceType.PKV + + private fun ProfilesData.Profile.isSsoValid() = singleSignOnTokenScope?.token?.isValid() ?: false + + private fun ProfilesData.Profile.isConsentDrawerNotShown() = !consentRepository.isConsentDrawerShown(id) + + private suspend fun ProfilesData.Profile.isConsentNotGranted() = + !(consentRepository.getConsent(id).map { consentRepository.isConsentGranted(it) }.getOrNull() ?: false) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentUseCase.kt deleted file mode 100644 index 74c6a5f4..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ShowGrantConsentUseCase.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.consent.usecase - -import de.gematik.ti.erp.app.consent.repository.ConsentRepository -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.repository.ProfileRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class ShowGrantConsentUseCase( - private val consentRepository: ConsentRepository, - private val profilesRepository: ProfileRepository -) { - operator fun invoke(): Flow = - profilesRepository.activeProfile().map { profile -> - with(profile) { - isPkv() && isConsentDrawerNotShown() && - isSsoValid() && isConsentNotGranted() - } - } - - private fun ProfilesData.Profile.isPkv() = insuranceType == ProfilesData.InsuranceType.PKV - - private fun ProfilesData.Profile.isSsoValid() = singleSignOnTokenScope?.token?.isValid() ?: false - - private fun ProfilesData.Profile.isConsentDrawerNotShown() = !consentRepository.isConsentDrawerShown(id) - - private suspend fun ProfilesData.Profile.isConsentNotGranted() = - !(consentRepository.getConsent(id).map { consentRepository.isConsentGranted(it) }.getOrNull() ?: false) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt index 08ffded4..3fd4ab51 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt @@ -1,24 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.AuthenticationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.AuthenticationPasswordEntityV1 import de.gematik.ti.erp.app.db.entities.v1.AvatarFigureV1 import de.gematik.ti.erp.app.db.entities.v1.IdpAuthenticationDataEntityV1 import de.gematik.ti.erp.app.db.entities.v1.IdpConfigurationEntityV1 @@ -26,19 +29,27 @@ import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.db.entities.v1.PasswordEntityV1 import de.gematik.ti.erp.app.db.entities.v1.PharmacySearchEntityV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.SettingsAuthenticationMethodV1 import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 import de.gematik.ti.erp.app.db.entities.v1.ShippingContactEntityV1 import de.gematik.ti.erp.app.db.entities.v1.SingleSignOnTokenScopeV1 import de.gematik.ti.erp.app.db.entities.v1.TruststoreEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InAppMessageEntity +import de.gematik.ti.erp.app.db.entities.v1.changelogs.InternalMessageEntity import de.gematik.ti.erp.app.db.entities.v1.invoice.ChargeableItemV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.InvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.PriceComponentV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationDosageEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationNotificationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationScheduleEntityV1 import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 import de.gematik.ti.erp.app.db.entities.v1.pharmacy.OftenUsedPharmacyEntityV1 import de.gematik.ti.erp.app.db.entities.v1.pharmacy.PharmacyCacheEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.AccidentTypeV1 import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.CoverageTypeV1 +import de.gematik.ti.erp.app.db.entities.v1.task.IdentifierEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.IngredientEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.InsuranceInformationEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationDispenseEntityV1 @@ -52,147 +63,246 @@ import de.gematik.ti.erp.app.db.entities.v1.task.QuantityEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 +import io.realm.kotlin.ext.copyFromRealm import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.RealmInstant import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.nanoseconds -const val ACTUAL_SCHEMA_VERSION = 30L +const val ACTUAL_SCHEMA_VERSION = 41L -val appSchemas = setOf( - AppRealmSchema( - version = ACTUAL_SCHEMA_VERSION, - classes = setOf( - SettingsEntityV1::class, - PharmacySearchEntityV1::class, - PasswordEntityV1::class, - TruststoreEntityV1::class, - IdpConfigurationEntityV1::class, - ProfileEntityV1::class, - CommunicationEntityV1::class, - MedicationEntityV1::class, - MedicationDispenseEntityV1::class, - MedicationRequestEntityV1::class, - OrganizationEntityV1::class, - PatientEntityV1::class, - PractitionerEntityV1::class, - ScannedTaskEntityV1::class, - SyncedTaskEntityV1::class, - IdpAuthenticationDataEntityV1::class, - AddressEntityV1::class, - InsuranceInformationEntityV1::class, - ShippingContactEntityV1::class, - IngredientEntityV1::class, - QuantityEntityV1::class, - RatioEntityV1::class, - PharmacyCacheEntityV1::class, - OftenUsedPharmacyEntityV1::class, - MultiplePrescriptionInfoEntityV1::class, - FavoritePharmacyEntityV1::class, - IngredientEntityV1::class, - PKVInvoiceEntityV1::class, - InvoiceEntityV1::class, - ChargeableItemV1::class, - PriceComponentV1::class - ), - migrateOrInitialize = { migrationStartedFrom -> - queryFirst() ?: run { - copyToRealm( - SettingsEntityV1() - ) - } - if (migrationStartedFrom < 3L) { - query().find().forEach { profile -> - profile.syncedTasks.forEach { syncedTask -> - syncedTask.parent = profile +@Requirement( + "O.Source_2#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The list of the model classes for the typed-safe databases.", + codeLines = 50 +) +@Suppress("CyclomaticComplexMethod") +fun appSchemas(profileName: String): Set { + return setOf( + AppRealmSchema( + version = ACTUAL_SCHEMA_VERSION, + classes = setOf( + SettingsEntityV1::class, + AuthenticationEntityV1::class, + AuthenticationPasswordEntityV1::class, + PharmacySearchEntityV1::class, + PasswordEntityV1::class, // TODO remove after migration 38 + TruststoreEntityV1::class, + IdpConfigurationEntityV1::class, + ProfileEntityV1::class, + CommunicationEntityV1::class, + MedicationEntityV1::class, + IdentifierEntityV1::class, + MedicationDispenseEntityV1::class, + MedicationRequestEntityV1::class, + OrganizationEntityV1::class, + PatientEntityV1::class, + PractitionerEntityV1::class, + ScannedTaskEntityV1::class, + SyncedTaskEntityV1::class, + IdpAuthenticationDataEntityV1::class, + AddressEntityV1::class, + InsuranceInformationEntityV1::class, + ShippingContactEntityV1::class, + IngredientEntityV1::class, + QuantityEntityV1::class, + RatioEntityV1::class, + PharmacyCacheEntityV1::class, + OftenUsedPharmacyEntityV1::class, + MultiplePrescriptionInfoEntityV1::class, + FavoritePharmacyEntityV1::class, + IngredientEntityV1::class, + PKVInvoiceEntityV1::class, + InvoiceEntityV1::class, + ChargeableItemV1::class, + PriceComponentV1::class, + InAppMessageEntity::class, + InternalMessageEntity::class, + MedicationDosageEntityV1::class, + MedicationNotificationEntityV1::class, + MedicationScheduleEntityV1::class + ), + migrateOrInitialize = { migrationStartedFrom -> + queryFirst() ?: run { + copyToRealm( + SettingsEntityV1() + ) + } + if (migrationStartedFrom < 3L) { + query().find().forEach { profile -> + profile.syncedTasks.forEach { syncedTask -> + syncedTask.parent = profile - syncedTask.communications.forEach { - it.parent = syncedTask - it.orderId = "" + syncedTask.communications.forEach { + it.parent = syncedTask + it.orderId = "" + } + } + profile.scannedTasks.forEach { scannedTask -> + scannedTask.parent = profile } - } - profile.scannedTasks.forEach { scannedTask -> - scannedTask.parent = profile } } - } - if (migrationStartedFrom < 10L) { - query().find().forEach { - it._avatarFigure = AvatarFigureV1.PersonalizedImage.toString() + if (migrationStartedFrom < 10L) { + query().find().forEach { + it._avatarFigure = AvatarFigureV1.PersonalizedImage.toString() + } } - } - if (migrationStartedFrom < 12L) { - query().find().forEach { - if (it._expirationDate?.isEmpty() == true) { - it._expirationDate = null + if (migrationStartedFrom < 12L) { + query().find().forEach { + if (it._expirationDate?.isEmpty() == true) { + it._expirationDate = null + } } } - } - if (migrationStartedFrom < 15L) { - query().find().forEach { - it.accidentType = AccidentTypeV1.None + if (migrationStartedFrom < 15L) { + query().find().forEach { + it.accidentType = AccidentTypeV1.None + } } - } - if (migrationStartedFrom < 17L) { - query().find().forEach { - if (it.lastAuthenticated != null) { - it._insuranceType = InsuranceTypeV1.GKV.toString() - } else { - it._insuranceType = InsuranceTypeV1.None.toString() + if (migrationStartedFrom < 17L) { + query().find().forEach { + if (it.lastAuthenticated != null) { + it._insuranceType = InsuranceTypeV1.GKV.toString() + } else { + it._insuranceType = InsuranceTypeV1.None.toString() + } } } - } - if (migrationStartedFrom < 18L) { - query().find().forEach { - it.invoices = realmListOf() + if (migrationStartedFrom < 18L) { + query().find().forEach { + it.invoices = realmListOf() + } + query().find().forEach { + if (it._authoredOn?.isEmpty() == true) { + it._authoredOn = null + } + } } - query().find().forEach { - if (it._authoredOn?.isEmpty() == true) { - it._authoredOn = null + + if (migrationStartedFrom < 19L) { + query().find().forEach { + if (it._handedOverOn?.isEmpty() == true) { + it._handedOverOn = null + } } } - } - if (migrationStartedFrom < 19L) { - query().find().forEach { - if (it._handedOverOn?.isEmpty() == true) { - it._handedOverOn = null + if (migrationStartedFrom < 23L) { + query().find().forEach { + it.lastModified = Instant.parse("2023-06-01T00:00:00Z").toRealmInstant() + it.isIncomplete = false + it.failureToReport = "" } } - } - if (migrationStartedFrom < 23L) { - query().find().forEach { - it.lastModified = Instant.parse("2023-06-01T00:00:00Z").toRealmInstant() - it.isIncomplete = false - it.failureToReport = "" + if (migrationStartedFrom < 27) { + query().find().groupBy { + it.scannedOn + }.forEach { + it.value.mapIndexed { index, scannedTaskEntityV1 -> + scannedTaskEntityV1.index = index + 1 + } + } } - } + // Logout all users with external authentication if they have not authenticated since 15.12.2023 (GID) + if (migrationStartedFrom < 30) { + query().find().forEach { + val epochSeconds: Long = Instant.parse("2023-12-15T00:00:00Z").epochSeconds + if (it.idpAuthenticationData?.singleSignOnTokenScope + == SingleSignOnTokenScopeV1.ExternalAuthentication + ) { + it.lastAuthenticated?.let { lastAuthenticated -> + if (lastAuthenticated < RealmInstant.from(epochSeconds, nanosecondAdjustment = 0)) { + it.idpAuthenticationData = null + } + } + } + } + } + if (migrationStartedFrom < 34) { + query().find().forEach { profileId -> + query("parent = $0", profileId).find().filter { it.name == null } + .groupBy { + it.scannedOn.toLocalDateTime().date + }.forEach { + it.value.mapIndexed { idx, scannedTaskEntityV1 -> + scannedTaskEntityV1.name = "Medikament ${idx + 1}" + // fix the ordering by adding minimal time to the scannedTask + scannedTaskEntityV1.scannedOn = ( + scannedTaskEntityV1.scannedOn.toInstant() + .plus(scannedTaskEntityV1.index.nanoseconds) + ).toRealmInstant() + } + } + } + } + if (migrationStartedFrom < 35) { + query().find().forEach { insuranceInformation -> + insuranceInformation._coverageType = CoverageTypeV1.UNKNOWN.name + } + } + if (migrationStartedFrom < 36) { + query().find().forEach { task -> + task.lastMedicationDispense = RealmInstant.MIN + } + } + if (migrationStartedFrom < 37) { + query().find().forEach { task -> + task.consumed = true + } + } + if (migrationStartedFrom < 38) { + query().find().forEach { settings -> + settings.authentication = AuthenticationEntityV1().apply { + when (settings.authenticationMethod) { + SettingsAuthenticationMethodV1.Password -> { + this.password = AuthenticationPasswordEntityV1().apply { + settings.password?.let { password -> + this.setHash(password._hash) + this.setSalt(password._salt) + } + } + } + + SettingsAuthenticationMethodV1.DeviceSecurity -> { + this.deviceSecurity = true + } - if (migrationStartedFrom < 27) { - query().find().groupBy { - it.scannedOn - }.forEach { - it.value.mapIndexed { index, scannedTaskEntityV1 -> - scannedTaskEntityV1.index = index + 1 + else -> { + // do nothing since onboarding was not done yet + } + } + this.failedAuthenticationAttempts = settings.authenticationFails + } } } - } - // Logout all users with external authentication if they have not authenticated since 15.12.2023 (GID) - if (migrationStartedFrom < 30) { - query().find().forEach { - val epochSeconds: Long = Instant.parse("2023-12-15T00:00:00Z").epochSeconds - if (it.idpAuthenticationData?.singleSignOnTokenScope - == SingleSignOnTokenScopeV1.ExternalAuthentication - ) { - it.lastAuthenticated?.let { lastAuthenticated -> - if (lastAuthenticated < RealmInstant.from(epochSeconds, nanosecondAdjustment = 0)) { - it.idpAuthenticationData = null + if (migrationStartedFrom < 39) { + query().find().forEach { profile -> + profile.isNewlyCreated = profile.name == profileName + } + } + + if (migrationStartedFrom < 41) { + query().find().forEach { syncedTask -> + for (medicationDispense in syncedTask.medicationDispenses) { + medicationDispense.medication = medicationDispense.medication?.copyFromRealm().apply { + this?.identifier = IdentifierEntityV1().apply { + this.pzn = medicationDispense.medication?.uniqueIdentifier + } + } + } + + syncedTask.medicationRequest?.medication = syncedTask.medicationRequest?.medication?.copyFromRealm().apply { + this?.identifier = IdentifierEntityV1().apply { + this.pzn = syncedTask.medicationRequest?.medication?.uniqueIdentifier } } } } } - } + ) ) -) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/QueryUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/QueryUtils.kt index 5e517d31..5c2def3a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/QueryUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/QueryUtils.kt @@ -1,27 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.db +import io.github.aakira.napier.Napier import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm -import io.realm.kotlin.types.RealmObject import io.realm.kotlin.TypedRealm +import io.realm.kotlin.types.RealmObject import kotlinx.coroutines.delay inline fun TypedRealm.queryFirst( @@ -52,6 +55,26 @@ suspend inline fun Realm.writeOrCopyToRealm( } } +suspend inline fun Realm.writeOrCopyToRealm( + crossinline factory: () -> T, + query: String = "TRUEPREDICATE", + vararg args: Any?, + crossinline block: MutableRealm.(T) -> R +): R? = + write { + try { + queryFirst(query, *args)?.let { + block(it) + } ?: run { + block(copyToRealm(factory())) + } + } catch (t: Throwable) { + cancelWrite() + Napier.e { "error writing into the database ${t.message}" } + throw t + } + } + /** * Queries [T] and calls [block] with the concrete instance of [T] as its receiver. * [block] will only be called if any object of type [T] is present. @@ -80,6 +103,10 @@ suspend inline fun Realm.writeToRealm( } } +@Deprecated( + message = "This uses a delay that causes problems, so it is deprecated in the next version", + replaceWith = ReplaceWith("writeOrCopyToRealm") +) suspend fun Realm.tryWrite(block: MutableRealm.() -> R): R { delay(100) return write { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt index 2dd99a16..65eb4c7b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Schema.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Schema.kt index 7e66efa8..d51117f7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Schema.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Schema.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.db import io.realm.kotlin.MutableRealm diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt index 0e05142d..ce70333b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtils.kt index 2a697c5f..f6090cc4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtils.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities @@ -54,6 +54,7 @@ fun Adjacent.objectIterator(): Iterator = } } +@Suppress("NestedBlockDepth") private suspend fun SequenceScope.flatten( currentObject: Cascading, currentDepth: Int, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Address.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Address.kt index 9a882c05..e4b14482 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Address.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Address.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Authentication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Authentication.kt new file mode 100644 index 00000000..b9f1e443 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Authentication.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1 + +import de.gematik.ti.erp.app.db.entities.Cascading +import de.gematik.ti.erp.app.db.entities.byteArrayBase64 +import io.realm.kotlin.Deleteable +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.Ignore + +class AuthenticationEntityV1 : RealmObject, Cascading { + var password: AuthenticationPasswordEntityV1? = null + var deviceSecurity: Boolean = false + var failedAuthenticationAttempts: Int = 0 + + override fun objectsToFollow(): Iterator = + iterator { + password?.let { yield(it) } + } +} + +class AuthenticationPasswordEntityV1 : RealmObject { + var _salt: String = "" + + @delegate:Ignore + var salt: ByteArray by byteArrayBase64(::_salt) + + var _hash: String = "" + + @delegate:Ignore + var hash: ByteArray by byteArrayBase64(::_hash) + + fun setSalt(salt: String) { + this._salt = salt + } + + fun setHash(hash: String) { + this._hash = hash + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpAuthenticationData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpAuthenticationData.kt index a3671a5c..f5d30919 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpAuthenticationData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpAuthenticationData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 @@ -29,7 +29,8 @@ enum class SingleSignOnTokenScopeV1 { ExternalAuthentication } -class IdpAuthenticationDataEntityV1 : RealmObject { +class IdpAuthenticationDataEntityV1 : + RealmObject { var singleSignOnToken: String? = null var _singleSignOnTokenScope: String = SingleSignOnTokenScopeV1.Default.toString() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpConfiguration.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpConfiguration.kt index 7d9dd162..17822538 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpConfiguration.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/IdpConfiguration.kt @@ -1,28 +1,34 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.db.entities.byteArrayBase64 import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore +@Requirement( + "A_20741#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Downloaded discovery document is saved in the database." +) class IdpConfigurationEntityV1 : RealmObject { var authorizationEndpoint: String = "" var ssoEndpoint: String = "" diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InAppMessageEntity.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InAppMessageEntity.kt new file mode 100644 index 00000000..0706b818 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InAppMessageEntity.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.changelogs + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey + +class InAppMessageEntity : RealmObject { + @PrimaryKey + var id: String = "" + var isUnRead: Boolean = true + var version: String = "" +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InternalMessageEntity.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InternalMessageEntity.kt new file mode 100644 index 00000000..e5d12a4b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/InternalMessageEntity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.changelogs + +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject + +class InternalMessageEntity : RealmObject { + var lastVersion: String? = null + var lastUpdatedVersion: String? = null + var counter: Long = 0 + var showWelcomeMessage: Boolean? = null + var inAppMessageEntity: RealmList = realmListOf() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt index 66251c8a..ee1f5848 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 @@ -25,11 +25,11 @@ import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 import io.realm.kotlin.Deleteable +import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore -import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.annotations.PrimaryKey import java.util.UUID @@ -106,6 +106,8 @@ class ProfileEntityV1 : RealmObject, Cascading { var active: Boolean = false + var isNewlyCreated: Boolean = false + var syncedTasks: RealmList = realmListOf() var scannedTasks: RealmList = realmListOf() var invoices: RealmList = realmListOf() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt index 04397d4d..4ec5114e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 @@ -30,6 +30,7 @@ import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore import kotlinx.datetime.LocalDateTime +// TODO remove after migration 38 enum class SettingsAuthenticationMethodV1 { HealthCard, DeviceSecurity, @@ -54,6 +55,7 @@ class PasswordEntityV1 : RealmObject { } } +// end remove class PharmacySearchEntityV1 : RealmObject { var name: String = "" var locationEnabled: Boolean = false @@ -64,25 +66,31 @@ class PharmacySearchEntityV1 : RealmObject { } @Requirement( - "O.Data_1", + "O.Data_1#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "The settings of the app offer maximum data protection and security on first app start." + - "User preferences are asked without discrimination within the onboarding process" + rationale = "All user permissions are set to false when the app starts and are changed " + + "only when the user modifies them." ) @Requirement( - "O.Data_13", - sourceSpecification = "BSI-eRp-ePA", - rationale = "default settings are not allow screenshots" + "A_24525#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Tracking is disabled by default." ) class SettingsEntityV1 : RealmObject, Cascading { + // TODO remove after migration 38 var _authenticationMethod: String = Unspecified.toString() @delegate:Ignore var authenticationMethod: SettingsAuthenticationMethodV1 by enumName(::_authenticationMethod) - var authenticationFails: Int = 0 + var password: PasswordEntityV1? = PasswordEntityV1() + + // end remove + var authentication: AuthenticationEntityV1? = AuthenticationEntityV1() + var zoomEnabled: Boolean = false var welcomeDrawerShown: Boolean = false + var time: RealmInstant = LocalDateTime(2021, 10, 15, 0, 0).toRealmInstant() var mainScreenTooltipsShown: Boolean = false var pharmacySearch: PharmacySearchEntityV1? = PharmacySearchEntityV1() @@ -93,8 +101,6 @@ class SettingsEntityV1 : RealmObject, Cascading { var dataProtectionVersionAccepted: RealmInstant = LocalDateTime(2021, 10, 15, 0, 0).toRealmInstant() - var password: PasswordEntityV1? = PasswordEntityV1() - var latestAppVersionName: String = "" var latestAppVersionCode: Int = -1 @@ -104,7 +110,21 @@ class SettingsEntityV1 : RealmObject, Cascading { var shippingContact: ShippingContactEntityV1? = null var mlKitAccepted: Boolean = false + @Requirement( + "O.Data_13#1", + "O.Resi_1#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Default settings does not allow screenshots", + codeLines = 3 + ) // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots + @Requirement( + "O.Data_13#1", + "O.Resi_1#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Default settings does not allow screenshots", + codeLines = 3 + ) var screenshotsAllowed: Boolean = false var trackingAllowed: Boolean = false diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/ShippingContact.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/ShippingContact.kt index e14baa41..833ce144 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/ShippingContact.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/ShippingContact.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Truststore.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Truststore.kt index c6469899..0f4f8cea 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Truststore.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Truststore.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/ChargeableItem.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/ChargeableItem.kt index 8918171c..63ca5da4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/ChargeableItem.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/ChargeableItem.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.invoice @@ -27,7 +27,7 @@ import io.realm.kotlin.types.annotations.Ignore enum class DescriptionTypeV1 { PZN, TA1, - HMNR, ; + HMNR } class ChargeableItemV1 : RealmObject, Cascading { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/Invoice.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/Invoice.kt index 99ae8f6d..9c547a53 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/Invoice.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/Invoice.kt @@ -1,28 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.invoice import de.gematik.ti.erp.app.db.entities.Cascading import io.realm.kotlin.Deleteable +import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject -import io.realm.kotlin.ext.realmListOf class InvoiceEntityV1 : RealmObject, Cascading { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PKVInvoice.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PKVInvoice.kt index 062b65f1..5ffd7195 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PKVInvoice.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PKVInvoice.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.invoice @@ -47,6 +47,8 @@ class PKVInvoiceEntityV1 : RealmObject, Cascading { var _whenHandedOver: String? = null + var consumed: Boolean = false + @delegate:Ignore var whenHandedOver: FhirTemporal? by temporalAccessorNullable(::_whenHandedOver) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PriceComponent.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PriceComponent.kt index 350b3708..9731acc0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PriceComponent.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/invoice/PriceComponent.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.invoice diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationDosageEntityV1.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationDosageEntityV1.kt new file mode 100644 index 00000000..64b8cbef --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationDosageEntityV1.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.medicationplan + +import io.realm.kotlin.types.RealmObject + +class MedicationDosageEntityV1 : RealmObject { + var form: String = "" + var ratio: String = "" +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationNotificationEntityV1.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationNotificationEntityV1.kt new file mode 100644 index 00000000..e3f48e5d --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationNotificationEntityV1.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.medicationplan + +import de.gematik.ti.erp.app.db.entities.Cascading +import io.realm.kotlin.Deleteable +import io.realm.kotlin.types.RealmObject + +class MedicationNotificationEntityV1 : RealmObject, Cascading { + var id: String = "" + var time: String = "" + var dosage: MedicationDosageEntityV1? = null + + override fun objectsToFollow(): Iterator = + iterator { + dosage?.let { yield(it) } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationScheduleEntityV1.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationScheduleEntityV1.kt new file mode 100644 index 00000000..98558748 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/medicationplan/MedicationScheduleEntityV1.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.medicationplan + +import de.gematik.ti.erp.app.db.entities.Cascading +import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 +import io.realm.kotlin.Deleteable +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject + +class MedicationScheduleEntityV1 : RealmObject, Cascading { + var start: String = "" + var end: String = "" + var isActive: Boolean = false + var amount: RatioEntityV1? = null + var profileId: String = "" + var title: String = "" + var body: String = "" + var taskId: String = "" + var notifications: RealmList = realmListOf() + + override fun objectsToFollow(): Iterator = + iterator { + yield(notifications) + amount?.let { yield(it) } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/FavoritePharmacy.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/FavoritePharmacy.kt index a6208c9b..24b4884c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/FavoritePharmacy.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/FavoritePharmacy.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.pharmacy diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/OftenUsedPharmacy.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/OftenUsedPharmacy.kt index 71f6d5a9..99b5f568 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/OftenUsedPharmacy.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/OftenUsedPharmacy.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.pharmacy diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/PharmacyCache.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/PharmacyCache.kt index fafc2ff4..9ba42c7c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/PharmacyCache.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/pharmacy/PharmacyCache.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.pharmacy diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Communication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Communication.kt index 5306f812..a2fc1f33 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Communication.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Communication.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -29,6 +29,7 @@ enum class CommunicationProfileV1 { class CommunicationEntityV1 : RealmObject { var taskId: String = "" + var communicationId: String = "" var orderId: String = "" diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Identifier.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Identifier.kt new file mode 100644 index 00000000..b687dbf5 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Identifier.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1.task + +import io.realm.kotlin.types.RealmObject + +class IdentifierEntityV1 : RealmObject { + var pzn: String? = null + var atc: String? = null + var ask: String? = null + var snomed: String? = null +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ingredient.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ingredient.kt index f493404c..45395850 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ingredient.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ingredient.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -27,10 +27,12 @@ class IngredientEntityV1 : RealmObject, Cascading { var form: String? = null var number: String? = null // ASK number (MedicationIngredient) / PZN (MedicationCompounding) var amount: String? = null + var identifier: IdentifierEntityV1? = IdentifierEntityV1() var strength: RatioEntityV1? = null override fun objectsToFollow(): Iterator = iterator { + identifier?.let { yield(it) } strength?.let { yield(it) } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/InsuranceInformation.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/InsuranceInformation.kt index 47f54b02..cadbce74 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/InsuranceInformation.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/InsuranceInformation.kt @@ -1,26 +1,53 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task +import de.gematik.ti.erp.app.db.entities.enumName import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.Ignore + +enum class CoverageTypeV1 { + GKV, // Gesetzliche Krankenversicherung + PKV, // Private Krankenversicherung + BG, // Berufsgenossenschaft + SEL, // Selbstzahler + SOZ, // Sozialamt + GPV, // Gesetzliche Pflegeversicherung + PPV, // Private Pflegeversicherung + BEI, // Beihilfe + UNKNOWN; + + companion object { + fun mapTo(value: String): CoverageTypeV1 = + try { + valueOf(value) + } catch (e: Throwable) { + UNKNOWN + } + } +} class InsuranceInformationEntityV1 : RealmObject { var name: String? = null var statusCode: String? = null + + @delegate:Ignore + var coverageType: CoverageTypeV1 by enumName(::_coverageType) + var _coverageType: String = CoverageTypeV1.UNKNOWN.toString() } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt index 3b16cabc..3bf528fb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -38,16 +38,8 @@ enum class MedicationCategoryV1 { UNKNOWN } -enum class MedicationProfileV1 { - PZN, COMPOUNDING, INGREDIENT, FREETEXT, UNKNOWN -} - class MedicationEntityV1 : RealmObject, Cascading { var text: String = "" - var _medicationProfile: String = MedicationProfileV1.PZN.toString() - - @delegate:Ignore - var medicationProfile: MedicationProfileV1 by enumName(::_medicationProfile) var _medicationCategory: String = MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL.toString() @delegate:Ignore @@ -58,7 +50,10 @@ class MedicationEntityV1 : RealmObject, Cascading { var manufacturingInstructions: String? = null var packaging: String? = null var normSizeCode: String? = null + + @Deprecated("use IdentifierEntityV1") var uniqueIdentifier: String? = null // PZN + var identifier: IdentifierEntityV1? = IdentifierEntityV1() var lotNumber: String? = null var _expirationDate: String? = null @@ -66,11 +61,13 @@ class MedicationEntityV1 : RealmObject, Cascading { @delegate:Ignore var expirationDate: FhirTemporal? by temporalAccessorNullable(::_expirationDate) + var ingredientMedications: RealmList = realmListOf() var ingredients: RealmList = realmListOf() override fun objectsToFollow(): Iterator = iterator { yield(ingredients) + identifier?.let { yield(it) } amount?.let { yield(it) } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationDispense.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationDispense.kt index 033e1a0a..9f2ce277 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationDispense.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationDispense.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -24,7 +24,6 @@ import de.gematik.ti.erp.app.utils.FhirTemporal import io.realm.kotlin.Deleteable import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore - class MedicationDispenseEntityV1 : RealmObject, Cascading { var dispenseId: String = "" var patientIdentifier: String = "" // KVNR diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationRequest.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationRequest.kt index c978a496..85591cb3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationRequest.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MedicationRequest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MultiplePrescription.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MultiplePrescription.kt index feafab65..4abdc8aa 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MultiplePrescription.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/MultiplePrescription.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Organization.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Organization.kt index 7debecef..4873172d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Organization.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Organization.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt index 38a39ebf..fd46f37f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Practitioner.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Practitioner.kt index 3ac6b0dd..08cb0a23 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Practitioner.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Practitioner.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Quantity.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Quantity.kt index 3f1a9101..d838d8ea 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Quantity.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Quantity.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ratio.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ratio.kt index 2de1f797..40093203 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ratio.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Ratio.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt index f6fa4bc6..3e49c2bb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -29,7 +29,7 @@ import io.realm.kotlin.types.RealmObject class ScannedTaskEntityV1 : RealmObject, Cascading { var taskId: String = "" var accessCode: String = "" - var name: String? = null + var name: String? = "" var index: Int = 0 var scannedOn: RealmInstant = RealmInstant.MIN var redeemedOn: RealmInstant? = null diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/SyncedTask.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/SyncedTask.kt index b78588d2..e29320cd 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/SyncedTask.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/SyncedTask.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1.task @@ -36,8 +36,9 @@ class SyncedTaskEntityV1 : RealmObject, Cascading { // Task Entities var taskId: String = "" - var accessCode: String? = null + var accessCode: String = "" var lastModified: RealmInstant = RealmInstant.MIN + var lastMedicationDispense: RealmInstant? = RealmInstant.MIN var expiresOn: RealmInstant? = null var acceptUntil: RealmInstant? = null @@ -56,6 +57,7 @@ class SyncedTaskEntityV1 : RealmObject, Cascading { var status: TaskStatusV1 by enumName(::_status) var medicationRequest: MedicationRequestEntityV1? = null + var medicationDispenses: RealmList = realmListOf() var communications: RealmList = realmListOf() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/di/JWSConverterFactory.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/di/JWSConverterFactory.kt index f12697e3..3f93f664 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/di/JWSConverterFactory.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/di/JWSConverterFactory.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.di diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt index 10e67032..6352f04d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt index 050b5696..5dfe2898 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -102,6 +102,7 @@ fun extractInsuranceInformation( processInsuranceInformation: InsuranceInformationFn ): InsuranceInformation { val name = resource.containedOrNull("payor")?.containedStringOrNull("display") + val statusCode = resource .findAll("extension") .filterWith( @@ -112,9 +113,12 @@ fun extractInsuranceInformation( ?.contained("valueCoding") ?.containedString("code") + val coverageType = resource.contained("type").contained("coding").containedString("code") + return processInsuranceInformation( name, - statusCode + statusCode, + coverageType ) } @@ -200,12 +204,12 @@ fun JsonElement.extractIngredient( ).firstOrNull() ?.containedStringOrNull("valueString") - val number = this.contained("itemCodeableConcept").containedOrNull("coding")?.containedString("code") + val identifier = parseIdentifier(this) return ingredientFn( text, form, - number, + identifier, amount, strength.extractRatio(ratioFn, quantityFn) ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapper.kt index 7fc059d3..7317e392 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapper.kt @@ -1,46 +1,51 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.utils.FhirTemporal -import de.gematik.ti.erp.app.utils.asFhirInstant import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull +import de.gematik.ti.erp.app.fhir.parser.containedOrNull import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.profileValue import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.utils.asFhirInstant import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive +// TODO: Can use kotlinx.serialization to create a Communication object and then deserilize to string for better performance /** * Template version 1.2 * Changes * - profile * - recipient.system + * */ + +// TODO: switch GEM_ERP_PR_Communication_DispReq to 1.4 between 15.01.2025 and 15.Jul.2025 +// (version 1.2 and 1.3 of GEM_ERP_PR_Communication_DispReq are valid until 15.Jul.2025) private fun templateVersion12( orderId: String, reference: String, @@ -111,11 +116,6 @@ enum class CommunicationProfile { ErxCommunicationDispReq, ErxCommunicationReply } -@Requirement( - "A_19984#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Validate incoming Communication data." -) fun extractCommunications( bundle: JsonElement, save: ( @@ -124,7 +124,7 @@ fun extractCommunications( orderId: String?, profile: CommunicationProfile, sentOn: FhirTemporal.Instant, - sender: String, + sender: String?, recipient: String, payload: String? ) -> Unit @@ -139,41 +139,24 @@ fun extractCommunications( .contained("profile") .contained() + // TODO: add version 1.4 for GEM_ERP_PR_Communication_DispReq and GEM_ERP_PR_Communication_Reply + // with changes and between 15.01.2025 and 15.Jul.2025 + // TODO: remove Version 1.2 and 1.3 after 15.Jul.2025 val profile = when { - profileValue("https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq").invoke(profileString) -> - CommunicationProfile.ErxCommunicationDispReq - - profileValue( - "https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq", - "1.1.1" - ).invoke( - profileString - ) -> - CommunicationProfile.ErxCommunicationDispReq - profileValue( "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_DispReq", - "1.2" + "1.2", + "1.3", + "1.4" ).invoke( profileString ) -> CommunicationProfile.ErxCommunicationDispReq - // without profile version - profileValue( - "https://gematik.de/fhir/StructureDefinition/ErxCommunicationReply" - ).invoke(profileString) -> - CommunicationProfile.ErxCommunicationReply - - profileValue( - "https://gematik.de/fhir/StructureDefinition/ErxCommunicationReply", - "1.1.1" - ).invoke(profileString) -> - CommunicationProfile.ErxCommunicationReply - profileValue( "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_Reply", - "1.2" + "1.2", + "1.3" ).invoke(profileString) -> CommunicationProfile.ErxCommunicationReply @@ -196,9 +179,9 @@ fun extractCommunications( } val sender = resource - .contained("sender") - .contained("identifier") - .containedString("value") + .containedOrNull("sender") + ?.contained("identifier") + ?.containedString("value") val recipient = resource .contained("recipient") @@ -220,6 +203,5 @@ fun extractCommunications( payload ) } - return bundleTotal } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationModel.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationModel.kt index e9b0c87c..a24df360 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationModel.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationModel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapper.kt index 6f92df10..16306a7c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapper.kt @@ -1,24 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull import de.gematik.ti.erp.app.fhir.parser.containedDouble @@ -30,9 +30,10 @@ import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.or import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.toFhirTemporal import de.gematik.ti.erp.app.utils.toFormattedDateTime -import de.gematik.ti.erp.app.invoice.model.InvoiceData import kotlinx.datetime.Instant import kotlinx.datetime.toInstant import kotlinx.serialization.json.JsonElement @@ -57,6 +58,11 @@ typealias InvoiceFn = ( additionalInformation: List ) -> R +@Requirement( + "O.Source_2#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Sanitization is also done for all FHIR mapping." +) fun extractInvoiceKBVAndErpPrBundle( bundle: JsonElement, process: ( @@ -109,7 +115,8 @@ fun extractInvoiceKBVAndErpPrBundle( profileString.isProfileValue( "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-AbgabedatenBundle", - "1.2" + "1.2", + "1.3" ) -> { invoiceBundle = resource } @@ -123,7 +130,9 @@ fun extractInvoiceKBVAndErpPrBundle( profileString.isProfileValue( "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Bundle", - "1.2" + "1.2", + "1.3", + "1.4" ) -> { erpPrBundle = resource } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt index 330412b4..e84e2bba 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt @@ -1,26 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.db.entities.v1.task.IdentifierEntityV1 import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.isProfileValue +import de.gematik.ti.erp.app.utils.FhirTemporal import kotlinx.serialization.json.JsonElement typealias AddressFn = ( @@ -53,7 +54,8 @@ typealias PractitionerFn = ( typealias InsuranceInformationFn = ( name: String?, - statusCode: String? + statusCode: String?, + typeCode: String ) -> R typealias MedicationRequestFn = ( @@ -78,9 +80,8 @@ typealias MultiplePrescriptionInfoFn = ( end: FhirTemporal? ) -> R -typealias MedicationFn = ( +typealias MedicationFn = ( text: String?, - medicationProfile: MedicationProfile, medicationCategory: MedicationCategory, form: String?, amount: Ratio?, @@ -88,7 +89,8 @@ typealias MedicationFn = ( manufacturingInstructions: String?, packaging: String?, normSizeCode: String?, - uniqueIdentifier: String?, + uniqueIdentifier: Identifier, + ingredientMedications: List, ingredients: List, lotNumber: String?, expirationDate: FhirTemporal? @@ -97,7 +99,7 @@ typealias MedicationFn = ( typealias IngredientFn = ( text: String, form: String?, - number: String?, + identifier: Identifier, amount: String?, strength: Ratio? ) -> R @@ -128,7 +130,23 @@ enum class AccidentType { } enum class MedicationProfile { - PZN, COMPOUNDING, INGREDIENT, FREETEXT, UNKNOWN + PZN, COMPOUNDING, INGREDIENT, FREETEXT, UNKNOWN, EPA +} + +data class Identifier( + val pzn: String? = null, + val atc: String? = null, + val ask: String? = null, + val snomed: String? = null +) { + fun toIdentifierEntityV1(): IdentifierEntityV1 { + return IdentifierEntityV1().apply { + pzn = this@Identifier.pzn + atc = this@Identifier.atc + ask = this@Identifier.ask + snomed = this@Identifier.snomed + } + } } @Suppress("LongParameterList") @@ -140,7 +158,7 @@ fun , processInsuranceInformation: InsuranceInformationFn, processAddress: AddressFn
, - processMedication: MedicationFn, + processMedication: MedicationFn, processIngredient: IngredientFn, processRatio: RatioFn, processQuantity: QuantityFn, @@ -164,20 +182,20 @@ fun extractKBVBundleVersion102( - bundle, - processOrganization, - processPatient, - processPractitioner, - processInsuranceInformation, - processAddress, - processMedication, - processIngredient, - processRatio, - processQuantity, - processMultiplePrescriptionInfo, - processMedicationRequest, - savePVSIdentifier, - save + bundle = bundle, + processOrganization = processOrganization, + processPatient = processPatient, + processPractitioner = processPractitioner, + processInsuranceInformation = processInsuranceInformation, + processAddress = processAddress, + processMedication = processMedication, + processIngredient = processIngredient, + processRatio = processRatio, + processQuantity = processQuantity, + processMultiplePrescriptionInfo = processMultiplePrescriptionInfo, + processMedicationRequest = processMedicationRequest, + savePVSIdentifier = savePVSIdentifier, + save = save ) profileString.isProfileValue( "https://fhir.kbv.de/StructureDefinition/KBV_PR_ERP_Bundle", diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt index a17b2205..c5cc756d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt @@ -1,35 +1,42 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArray import de.gematik.ti.erp.app.fhir.parser.containedBooleanOrNull import de.gematik.ti.erp.app.fhir.parser.containedOrNull import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull +import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue +import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive +@Requirement( + "O.Source_2#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Sanitization is also done for all FHIR mapping." +) typealias MedicationDispenseFn = ( dispenseId: String, patientIdentifier: String, // KVNR @@ -43,7 +50,7 @@ typealias MedicationDispenseFn = ( fun extractMedicationDispense( resource: JsonElement, processMedicationDispense: MedicationDispenseFn, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn @@ -76,9 +83,47 @@ fun extractMedicat ) } +fun extractMedicationDispenseWithMedication( + medicationDispense: JsonElement, + medication: JsonElement, + processMedicationDispense: MedicationDispenseFn, + processMedication: MedicationFn, + ingredientFn: IngredientFn, + ratioFn: RatioFn, + quantityFn: QuantityFn +): MedicationDispense { + val dispenseId = medicationDispense.containedString("id") + val patientIdentifier = medicationDispense.contained("subject").contained("identifier").containedString("value") + val dispenseMedication = extractDispenseMedication( + medication, + processMedication, + ingredientFn, + ratioFn, + quantityFn + ) + + val wasSubstituted = medicationDispense.containedOrNull("substitution") + ?.containedBooleanOrNull("wasSubstituted") ?: false + val dosageInstruction = medicationDispense.containedOrNull("dosageInstruction")?.containedStringOrNull("text") + val performer = medicationDispense.containedArray("performer")[0] + .contained("actor").contained("identifier").containedString("value") // Telematik-ID + val whenHandedOver = medicationDispense.contained("whenHandedOver").jsonPrimitive.toFhirTemporal() + ?: error("error on parsing date of delivery") + + return processMedicationDispense( + dispenseId, + patientIdentifier, + dispenseMedication, + wasSubstituted, + dosageInstruction, + performer, + whenHandedOver + ) +} + fun extractDispenseMedication( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn @@ -129,20 +174,55 @@ fun extractDispenseMedication( "1.1.0" ) -> extractMedicationFreetextVersion110(resource, quantityFn, ratioFn, processMedication) - else -> processMedication( - "", - MedicationProfile.UNKNOWN, - MedicationCategory.UNKNOWN, - null, - null, - false, - null, - null, - null, - null, - listOf(), - null, + profileString.isProfileValue( + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication", + "1.4" + ) -> extractEpaMedications(resource, quantityFn, ratioFn, ingredientFn, processMedication) + + profileString.isProfileValue( + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pharmaceutical-product" + ) -> extractEpaMedications(resource, quantityFn, ratioFn, ingredientFn, processMedication) + + else -> + processMedication( + "", + MedicationCategory.UNKNOWN, + null, + null, + false, + null, + null, + null, + Identifier(), + listOf(), + listOf(), + null, + null + ) + } +} + +fun extractMedicationDispensePairs(bundle: JsonElement): List> { + val resources = bundle.findAll("entry.resource").toList() + val medicationDispenses = resources.filter { + it.containedString("resourceType") == "MedicationDispense" + } + val medications = resources.filter { + it.containedString("resourceType") == "Medication" + } + + return medicationDispenses.mapNotNull { dispense -> + val medicationReference = dispense + .contained("medicationReference") + .containedString("reference") + .substringAfter("urn:uuid:") + val medication = medications.find { + it.contained("id").containedString() == medicationReference + } + if (medication != null) { + Pair(dispense, medication) + } else { null - ) + } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt index 78128d88..b8c2a835 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -26,6 +26,7 @@ import de.gematik.ti.erp.app.fhir.parser.containedDouble import de.gematik.ti.erp.app.fhir.parser.containedInt import de.gematik.ti.erp.app.fhir.parser.containedIntOrNull import de.gematik.ti.erp.app.fhir.parser.containedObject +import de.gematik.ti.erp.app.fhir.parser.containedObjectOrNull import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull import de.gematik.ti.erp.app.fhir.parser.filterWith @@ -33,13 +34,13 @@ import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.not import de.gematik.ti.erp.app.fhir.parser.or import de.gematik.ti.erp.app.fhir.parser.stringValue +import kotlinx.datetime.DayOfWeek import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.JsonElement import java.net.MalformedURLException import java.net.URL -import kotlinx.datetime.DayOfWeek val Contained = listOf("contained") val TypeCodingCode = listOf("type", "coding", "code") @@ -51,11 +52,17 @@ const val OnlineServiceRank = 300 /** * Extract pharmacy services from a search bundle. */ -@Requirement( +/*( "A_19984#2", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Validate incoming Pharmacy data." +) */ +@Requirement( + "O.Source_2#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Sanitization is also done for all FHIR mapping." ) +@Suppress("CyclomaticComplexMethod") fun extractPharmacyServices( bundle: JsonElement, onError: (JsonElement, Exception) -> Unit = { _, _ -> } @@ -67,7 +74,7 @@ fun extractPharmacyServices( val pharmacies = resources.mapCatching(onError) { pharmacy -> val locationId = pharmacy.containedString("id") val locationName = pharmacy.containedString("name") - val localService = LocalPharmacyService( + val localService = PharmacyService.LocalPharmacyService( name = locationName, openingHours = pharmacy.containedArrayOrNull("hoursOfOperation")?.let { hoursOfOperation(it) } ?: OpeningHours(emptyMap()) @@ -79,7 +86,7 @@ fun extractPharmacyServices( .filterWith(TypeCodingCode, stringValue("498")) .firstOrNull() ?.let { service -> - DeliveryPharmacyService( + PharmacyService.DeliveryPharmacyService( name = locationName, openingHours = service.containedArrayOrNull("availableTime")?.let { availableTime(it) } ?: OpeningHours(emptyMap()) @@ -123,19 +130,19 @@ fun extractPharmacyServices( } val pickUpPharmacyService = if (isOutpatientPharmacy) { - PickUpPharmacyService(name = locationName) + PharmacyService.PickUpPharmacyService(name = locationName) } else { null } val onlinePharmacyService = if (isMobilePharmacy) { - OnlinePharmacyService(name = locationName) + PharmacyService.OnlinePharmacyService(name = locationName) } else { null } - val position = pharmacy.containedObject("position").let { - Location( + val position = pharmacy.containedObjectOrNull("position")?.let { + Coordinates( latitude = it.containedDouble("latitude"), longitude = it.containedDouble("longitude") ) @@ -144,7 +151,7 @@ fun extractPharmacyServices( Pharmacy( id = locationId, name = locationName, - location = position, + coordinates = position, address = pharmacy.containedObject("address").let { address -> PharmacyAddress( lines = address.containedArray("line").map { it.containedString() }, @@ -223,6 +230,7 @@ private fun sanitizeUrl(url: String): String = "" } +@Suppress("CyclomaticComplexMethod") private fun contacts( telecom: JsonArray ): PharmacyContacts { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacySearchModel.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacySearchModel.kt index 95b5f635..19d76917 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacySearchModel.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacySearchModel.kt @@ -1,26 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.fhir.model import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime +import kotlinx.serialization.Serializable import kotlin.math.PI import kotlin.math.abs import kotlin.math.asin @@ -38,14 +41,15 @@ data class PharmacyServices( val bundleResultCount: Int ) -data class Location( +@Serializable +data class Coordinates( val latitude: Double, val longitude: Double ) { /** * Haversine distance between two points on a sphere. */ - fun distanceInMeters(other: Location): Double { + private fun distanceInMeters(other: Coordinates): Double { val dLat = toRadians(other.latitude - this.latitude) val dLon = toRadians(other.longitude - this.longitude) val lat1 = toRadians(this.latitude) @@ -56,9 +60,9 @@ data class Location( } private fun toRadians(deg: Double) = deg / 180.0 * PI - operator fun minus(other: Location) = distanceInMeters(other) + operator fun minus(other: Coordinates) = distanceInMeters(other) override fun equals(other: Any?): Boolean = - if (other == null || other !is Location) { + if (other == null || other !is Coordinates) { false } else { abs(this.latitude - other.latitude) < EqEpsilon && abs(this.longitude - other.longitude) < EqEpsilon @@ -71,12 +75,14 @@ data class Location( } } +@Serializable data class PharmacyAddress( val lines: List, val postalCode: String, val city: String ) +@Serializable data class PharmacyContacts( val phone: String, val mail: String, @@ -90,63 +96,17 @@ data class Pharmacy( val id: String, val name: String, val address: PharmacyAddress, - val location: Location, + val coordinates: Coordinates? = null, val contacts: PharmacyContacts, val provides: List, val telematikId: String ) -sealed interface PharmacyService - -interface TemporalPharmacyService : PharmacyService { - val openingHours: OpeningHours - fun isOpenAt(tm: LocalDateTime) = openingHours.isOpenAt(tm) - fun isAllDayOpen(day: DayOfWeek) = openingHours[day]?.any { it.isAllDayOpen() } ?: false - fun openUntil(tm: LocalDateTime): LocalTime? { - val localTm = tm.time - return openingHours[tm.dayOfWeek]?.find { - it.isOpenAt(localTm) - }?.closingTime - } - - fun opensAt(tm: LocalDateTime): LocalTime? { - val localTm = tm.time - return openingHours[tm.dayOfWeek]?.find { - if (it.openingTime == null) { - true - } else { - it.openingTime >= localTm - } - }?.openingTime - } -} - -data class OnlinePharmacyService( - val name: String -) : PharmacyService - -data class PickUpPharmacyService( - val name: String -) : PharmacyService - -data class DeliveryPharmacyService( - val name: String, - override val openingHours: OpeningHours -) : TemporalPharmacyService - -data class EmergencyPharmacyService( - val name: String, - override val openingHours: OpeningHours -) : TemporalPharmacyService - -data class LocalPharmacyService( - val name: String, - override val openingHours: OpeningHours -) : TemporalPharmacyService - +@Serializable data class OpeningHours(val openingTime: Map>) : Map> by openingTime +@Serializable data class OpeningTime( val openingTime: LocalTime?, val closingTime: LocalTime? diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyService.kt new file mode 100644 index 00000000..5ca57e86 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyService.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.fhir.model.PharmacyServiceSerializationType.DeliveryPharmacyServiceType +import de.gematik.ti.erp.app.fhir.model.PharmacyServiceSerializationType.EmergencyPharmacyServiceType +import de.gematik.ti.erp.app.fhir.model.PharmacyServiceSerializationType.LocalPharmacyServiceType +import de.gematik.ti.erp.app.fhir.model.PharmacyServiceSerializationType.OnlinePharmacyServiceType +import de.gematik.ti.erp.app.fhir.model.PharmacyServiceSerializationType.PickUpPharmacyServiceType +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.time.DayOfWeek + +/* + Since kotlinx.serialization does not support PolymorphicSerializer of sealed interfaces + out of the box we need to add a type to let the serializer know the difference if it is + a sealed class or sealed interface. +*/ +enum class PharmacyServiceSerializationType { + OnlinePharmacyServiceType, + PickUpPharmacyServiceType, + DeliveryPharmacyServiceType, + EmergencyPharmacyServiceType, + LocalPharmacyServiceType +} + +@Serializable(with = PharmacyServiceSerializer::class) +sealed interface PharmacyService { + + val name: String + val type: PharmacyServiceSerializationType + + @Serializable + data class OnlinePharmacyService( + override val name: String, + override val type: PharmacyServiceSerializationType = OnlinePharmacyServiceType + ) : PharmacyService + + @Serializable + data class PickUpPharmacyService( + override val name: String, + override val type: PharmacyServiceSerializationType = PickUpPharmacyServiceType + ) : PharmacyService + + @Serializable + data class DeliveryPharmacyService( + override val name: String, + override val openingHours: OpeningHours, + override val type: PharmacyServiceSerializationType = DeliveryPharmacyServiceType + ) : TemporalPharmacyService, PharmacyService + + @Serializable + data class EmergencyPharmacyService( + override val name: String, + override val openingHours: OpeningHours, + override val type: PharmacyServiceSerializationType = EmergencyPharmacyServiceType + ) : TemporalPharmacyService, PharmacyService + + @Serializable + data class LocalPharmacyService( + override val name: String, + override val openingHours: OpeningHours, + override val type: PharmacyServiceSerializationType = LocalPharmacyServiceType + ) : TemporalPharmacyService, PharmacyService +} + +object PharmacyServiceSerializer : JsonContentPolymorphicSerializer(PharmacyService::class) { + override fun selectDeserializer(element: JsonElement): KSerializer { + element.jsonObject["type"]?.jsonPrimitive?.content?.let { classType -> + return when (PharmacyServiceSerializationType.valueOf(classType)) { + OnlinePharmacyServiceType -> PharmacyService.OnlinePharmacyService.serializer() + PickUpPharmacyServiceType -> PharmacyService.PickUpPharmacyService.serializer() + DeliveryPharmacyServiceType -> PharmacyService.DeliveryPharmacyService.serializer() + EmergencyPharmacyServiceType -> PharmacyService.EmergencyPharmacyService.serializer() + LocalPharmacyServiceType -> PharmacyService.LocalPharmacyService.serializer() + } + } + ?: throw SerializationException( + "PharmacyServiceSerializer: key 'type' not found or does not matches any module type" + ) + } +} + +interface TemporalPharmacyService /*: PharmacyService*/ { + val openingHours: OpeningHours + fun isOpenAt(tm: LocalDateTime) = openingHours.isOpenAt(tm) + fun isAllDayOpen(day: DayOfWeek) = openingHours[day]?.any { it.isAllDayOpen() } ?: false + fun openUntil(localDateTime: LocalDateTime): LocalTime? { + val localTime = localDateTime.time + return openingHours[localDateTime.dayOfWeek]?.find { + it.isOpenAt(localTime) + }?.closingTime + } + + fun opensAt(localDateTime: LocalDateTime): LocalTime? { + val localTime = localDateTime.time + return openingHours[localDateTime.dayOfWeek]?.find { + if (it.openingTime == null) { + true + } else { + it.openingTime >= localTime + } + }?.openingTime + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperEpaVersion_1_4.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperEpaVersion_1_4.kt new file mode 100644 index 00000000..5f5b4d8b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperEpaVersion_1_4.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedBoolean +import de.gematik.ti.erp.app.fhir.parser.containedOrNull +import de.gematik.ti.erp.app.fhir.parser.containedString +import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull +import de.gematik.ti.erp.app.fhir.parser.filterWith +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.toFhirTemporal +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive + +fun extractEpaMedications( + resource: JsonElement, + quantityFn: QuantityFn, + ratioFn: RatioFn, + ingredientFn: IngredientFn, + processMedication: MedicationFn +): Medication { + val containedMedications = + resource.findAll("contained") + .filterWith( + "resourceType", + stringValue("Medication") + ) + .map { + extractContainedMedication(it, quantityFn, ratioFn, ingredientFn, processMedication) + }.toList() + + val text = resource.containedOrNull("code")?.containedStringOrNull("text") + ?: resource.containedOrNull("code")?.contained("coding")?.containedStringOrNull("code") + val medicationCategory = extractMedicationCategoryEpa(resource) + val form = resource.containedOrNull("form") + ?.findAll("coding") + ?.filterWith( + "system", + stringValue( + "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM" + ) + ) + ?.firstOrNull() + ?.containedString("code") + + val amount = resource.containedOrNull("amount")?.extractRatio(ratioFn, quantityFn) + val vaccine = resource.findAll("extension") + .filterWith( + "url", + stringValue("https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension") + ) + .firstOrNull() + ?.containedBoolean("valueBoolean") ?: false + + val normSizeCode = resource.findAll("extension") + .filterWith( + "url", + stringValue("http://fhir.de/StructureDefinition/normgroesse") + ) + .firstOrNull() + ?.containedStringOrNull("valueCode") + + val identifier = parseIdentifier(resource) + + val manufacturingInstructions = resource.findAll("extension") + .filterWith( + "url", + stringValue( + "https://gematik.de/fhir/epa-medication/StructureDefinition/" + + "medication-manufacturing-instructions-extension" + ) + ) + .firstOrNull() + ?.containedString("valueString") + + val packaging = resource.findAll("extension") + .filterWith( + "url", + stringValue( + "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-formulation-packaging-extension" + ) + ) + .firstOrNull() + ?.containedString("valueString") + + val ingredients = resource.findAll("ingredient").filter { + it.containedOrNull("itemReference")?.containedStringOrNull("reference").isNullOrEmpty() + }.map { + it.extractEpaIngredient(ingredientFn, ratioFn, quantityFn) + }.toList() + + val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") + val expirationDate = resource.containedOrNull("batch") + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() + + return processMedication( + text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + containedMedications, + ingredients, + lotNumber, + expirationDate + ) +} + +fun extractContainedMedication( + resource: JsonElement, + quantityFn: QuantityFn, + ratioFn: RatioFn, + ingredientFn: IngredientFn, + processIngredientMedication: MedicationFn +): Medication { + val text = resource.contained("code").containedStringOrNull("text") + val medicationCategory = extractMedicationCategoryEpa(resource) + val form = resource.containedOrNull("form") + ?.findAll("coding") + ?.filterWith( + "system", + stringValue( + "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM" + ) + ) + ?.firstOrNull() + ?.containedString("code") + + val amount = resource.containedOrNull("amount")?.extractRatio(ratioFn, quantityFn) + val vaccine = resource.findAll("extension") + .filterWith( + "url", + stringValue("https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension") + ) + .firstOrNull() + ?.containedBoolean("valueBoolean") ?: false + + val normSizeCode = resource.findAll("extension") + .filterWith( + "url", + stringValue("http://fhir.de/StructureDefinition/normgroesse") + ) + .firstOrNull() + ?.containedStringOrNull("valueCode") + + val identifier = parseIdentifier(resource) + + val manufacturingInstructions = resource.findAll("extension") + .filterWith( + "url", + stringValue( + "https://gematik.de/fhir/epa-medication/StructureDefinition/" + + "medication-manufacturing-instructions-extension" + ) + ) + .firstOrNull() + ?.containedString("valueString") + + val packaging = resource.findAll("extension") + .filterWith( + "url", + stringValue( + "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-formulation-packaging-extension" + ) + ) + .firstOrNull() + ?.containedString("valueString") + + val ingredients = resource.findAll("ingredient").filter { + it.containedOrNull("itemReference")?.containedStringOrNull("reference").isNullOrEmpty() + }.map { + it.extractEpaIngredient(ingredientFn, ratioFn, quantityFn) + }.toList() + + val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") + val expirationDate = resource.containedOrNull("batch") + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() + + return processIngredientMedication( + text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + emptyList(), + ingredients, + lotNumber, + expirationDate + ) +} + +fun JsonElement.extractEpaIngredient( + ingredientFn: IngredientFn, + ratioFn: RatioFn, + quantityFn: QuantityFn +): Ingredient { + val text = this.containedOrNull("itemCodeableConcept")?.contained("coding")?.containedStringOrNull("display") ?: "" + val strength = this.contained("strength") + val amount = strength.findAll("extension").filterWith( + "url", + stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Ingredient_Amount") + ).firstOrNull() + ?.containedStringOrNull("valueString") + val form = this.findAll("extension").filterWith( + "url", + stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Ingredient_Form") + ).firstOrNull() + ?.containedStringOrNull("valueString") + + val identifier = parseIdentifier(this) + + return ingredientFn( + text, + form, + identifier, + amount, + strength.extractRatio(ratioFn, quantityFn) + ) +} + +fun parseIdentifier(resource: JsonElement): Identifier { + val pzn = extractCode(resource, "http://fhir.de/CodeSystem/ifa/pzn") + val atc = extractCode(resource, "http://fhir.de/CodeSystem/bfarm/atc") + val ask = extractCode(resource, "http://fhir.de/CodeSystem/ask") + val snomed = extractCode(resource, "http://snomed.info/sct") + + return Identifier(pzn = pzn, atc = atc, ask = ask, snomed = snomed) +} + +fun extractCode(resource: JsonElement, systemUrl: String): String? { + return resource.containedOrNull("code")?.findAll("coding") + ?.filterWith("system", stringValue(systemUrl)) + ?.firstOrNull()?.containedString("code") + ?: resource.containedOrNull("itemCodeableConcept")?.findAll("coding") + ?.filterWith("system", stringValue(systemUrl)) + ?.firstOrNull()?.containedString("code") +} + +fun extractMedicationCategoryEpa(resource: JsonElement): MedicationCategory { + val medicationCategoryCode = resource + .findAll("extension") + .filterWith( + "url", + stringValue("https://gematik.de/fhir/epa-medication/StructureDefinition/drug-category-extension") + ) + .firstOrNull() + ?.contained("valueCoding") + ?.containedStringOrNull("code") + + return when (medicationCategoryCode) { + "00" -> MedicationCategory.ARZNEI_UND_VERBAND_MITTEL + "01" -> MedicationCategory.BTM + "02" -> MedicationCategory.AMVV + else -> MedicationCategory.UNKNOWN + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt index 7a115eb9..5755f385 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("UnusedPrivateMember") + package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.asFhirLocalDate import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedBoolean import de.gematik.ti.erp.app.fhir.parser.containedDouble @@ -29,6 +30,7 @@ import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.asFhirLocalDate import de.gematik.ti.erp.app.utils.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive @@ -42,7 +44,7 @@ fun , processInsuranceInformation: InsuranceInformationFn, processAddress: AddressFn
, - processMedication: MedicationFn, + processMedication: MedicationFn, processIngredient: IngredientFn, processRatio: RatioFn, processQuantity: QuantityFn, @@ -319,12 +321,11 @@ fun extractPatient( fun extractPZNMedication( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.PZN val medicationCategory = extractMedicationCategory(resource) val form = resource.containedOrNull("form") @@ -355,10 +356,7 @@ fun extractPZNMedication( .firstOrNull() ?.containedStringOrNull("valueCode") - val uniqueIdentifier = - resource.contained("code").findAll("coding") - .filterWith("system", stringValue("http://fhir.de/CodeSystem/ifa/pzn")) - .firstOrNull()?.containedString("code") + val identifier = parseIdentifier(resource) val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") @@ -366,7 +364,6 @@ fun extractPZNMedication( return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -374,8 +371,9 @@ fun extractPZNMedication( null, null, normSizeCode, - uniqueIdentifier, - listOf(), + identifier, + emptyList(), + emptyList(), lotNumber, expirationDate ) @@ -383,13 +381,12 @@ fun extractPZNMedication( fun extractMedicationCompounding( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.COMPOUNDING val medicationCategory = extractMedicationCategory(resource) val form = resource.contained("form").containedString("text") val amount = resource.containedOrNull("amount")?.extractRatio(ratioFn, quantityFn) @@ -420,13 +417,14 @@ fun extractMedicationCompounding( it.extractIngredient(ingredientFn, ratioFn, quantityFn) }.toList() + val identifier = parseIdentifier(resource) + val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -434,7 +432,8 @@ fun extractMedicationCompounding( manufacturingInstructions, packaging, null, - null, + identifier, + emptyList(), ingredients, lotNumber, expirationDate @@ -445,13 +444,11 @@ fun extractMedicationFreetext( resource: JsonElement, quantityFn: QuantityFn, // needed for medication alias ratioFn: RatioFn, // needed for medication alias - processMedication: MedicationFn + processMedication: MedicationFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.FREETEXT val medicationCategory = extractMedicationCategory(resource) val form = resource.containedOrNull("form")?.containedStringOrNull("text") - val vaccine = resource.findAll("extension") .filterWith( "url", @@ -466,7 +463,6 @@ fun extractMedicationFreetext( return processMedication( text, - medicationProfile, medicationCategory, form, null, @@ -474,7 +470,8 @@ fun extractMedicationFreetext( null, null, null, - null, + Identifier(), + emptyList(), listOf(), lotNumber, expirationDate @@ -483,13 +480,12 @@ fun extractMedicationFreetext( fun extractMedicationIngredient( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.INGREDIENT val medicationCategory = extractMedicationCategory(resource) val form = resource.containedOrNull("form")?.containedStringOrNull("text") @@ -515,13 +511,14 @@ fun extractMedicationIngredient( it.extractIngredient(ingredientFn, ratioFn, quantityFn) }.toList() + val identifier = parseIdentifier(resource) + val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -529,7 +526,8 @@ fun extractMedicationIngredient( null, null, normSizeCode, - null, + identifier, + emptyList(), ingredients, lotNumber, expirationDate diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt index d2fc691c..ac4c220a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("UnusedPrivateMember") + package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.asFhirLocalDate import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedBoolean import de.gematik.ti.erp.app.fhir.parser.containedBooleanOrNull @@ -30,6 +31,7 @@ import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.asFhirLocalDate import de.gematik.ti.erp.app.utils.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive @@ -43,7 +45,7 @@ fun , processInsuranceInformation: InsuranceInformationFn, processAddress: AddressFn
, - processMedication: MedicationFn, + processMedication: MedicationFn, processIngredient: IngredientFn, processRatio: RatioFn, processQuantity: QuantityFn, @@ -222,22 +224,22 @@ fun extractMedica .findAll("extension") .filterWith( "url", - stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Accident") + stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_FOR_Accident") ).firstOrNull() val dateOfAccident = accidentInformation?.findAll("extension")?.filterWith( "url", - stringValue("unfalltag") + stringValue("Unfalltag") )?.firstOrNull()?.containedOrNull("valueDate")?.jsonPrimitive?.asFhirLocalDate() val location = accidentInformation?.findAll("extension")?.filterWith( "url", - stringValue("unfallbetrieb") + stringValue("Unfallbetrieb") )?.firstOrNull() ?.containedString("valueString") val accidentTypeCode = accidentInformation?.findAll("extension")?.filterWith( "url", - stringValue("unfallkennzeichen") + stringValue("Unfallkennzeichen") )?.firstOrNull() ?.containedOrNull("valueCoding") ?.containedStringOrNull("code") @@ -282,7 +284,7 @@ fun extractMedica .findAll("extension") .filterWith( "url", - stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_StatusCoPayment") + stringValue("https://fhir.kbv.de/StructureDefinition/KBV_EX_FOR_StatusCoPayment") ) .firstOrNull() ?.containedOrNull("valueCoding") @@ -345,12 +347,11 @@ fun extractPatientVersion110( fun extractPZNMedicationVersion110( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.PZN val medicationCategory = extractMedicationCategoryVerion110(resource) val form = resource.containedOrNull("form") ?.findAll("coding") @@ -381,10 +382,7 @@ fun extractPZNMedicationVersion110( .firstOrNull() ?.containedStringOrNull("valueCode") - val uniqueIdentifier = - resource.contained("code").findAll("coding") - .filterWith("system", stringValue("http://fhir.de/CodeSystem/ifa/pzn")) - .firstOrNull()?.containedString("code") + val identifier = parseIdentifier(resource) val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") @@ -392,7 +390,6 @@ fun extractPZNMedicationVersion110( return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -400,21 +397,21 @@ fun extractPZNMedicationVersion110( null, null, normSizeCode, - uniqueIdentifier, - listOf(), + identifier, + emptyList(), + emptyList(), lotNumber, expirationDate ) } fun extractMedicationCompoundingVersion110( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.COMPOUNDING val medicationCategory = extractMedicationCategoryVerion110(resource) val form = resource.containedOrNull("form")?.containedStringOrNull("text") @@ -447,13 +444,13 @@ fun extractMedicationCompoundingVersio it.extractIngredient(ingredientFn, ratioFn, quantityFn) }.toList() + val identifier = parseIdentifier(resource) val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -461,7 +458,8 @@ fun extractMedicationCompoundingVersio manufacturingInstructions, packaging, null, - null, + identifier, + emptyList(), ingredients, lotNumber, expirationDate @@ -470,13 +468,12 @@ fun extractMedicationCompoundingVersio fun extractMedicationIngredientVersion110( resource: JsonElement, - processMedication: MedicationFn, + processMedication: MedicationFn, ingredientFn: IngredientFn, ratioFn: RatioFn, quantityFn: QuantityFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.INGREDIENT val medicationCategory = extractMedicationCategoryVerion110(resource) val form = resource.containedOrNull("form")?.containedStringOrNull("text") @@ -502,13 +499,14 @@ fun extractMedicationIngredientVersion it.extractIngredient(ingredientFn, ratioFn, quantityFn) }.toList() + val identifier = parseIdentifier(resource) + val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, - medicationProfile, medicationCategory, form, amount, @@ -516,7 +514,8 @@ fun extractMedicationIngredientVersion null, null, normSizeCode, - null, + identifier, + emptyList(), ingredients, lotNumber, expirationDate @@ -527,10 +526,9 @@ fun extractMedicationFreetextVersion11 resource: JsonElement, quantityFn: QuantityFn, // needed for medication alias ratioFn: RatioFn, // needed for medication alias - processMedication: MedicationFn + processMedication: MedicationFn ): Medication { val text = resource.contained("code").containedStringOrNull("text") - val medicationProfile = MedicationProfile.FREETEXT val medicationCategory = extractMedicationCategoryVerion110(resource) val form = resource.containedOrNull("form")?.containedStringOrNull("text") @@ -548,7 +546,6 @@ fun extractMedicationFreetextVersion11 return processMedication( text, - medicationProfile, medicationCategory, form, null, @@ -556,8 +553,9 @@ fun extractMedicationFreetextVersion11 null, null, null, - null, - listOf(), + Identifier(), + emptyList(), + emptyList(), lotNumber, expirationDate ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt index 2185684d..0d4bebec 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt @@ -1,33 +1,37 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.FhirTemporal -import de.gematik.ti.erp.app.utils.asFhirInstant -import de.gematik.ti.erp.app.utils.asFhirLocalDate +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull +import de.gematik.ti.erp.app.fhir.parser.containedOrNull import de.gematik.ti.erp.app.fhir.parser.containedString +import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.utils.asFhirInstant +import de.gematik.ti.erp.app.utils.asFhirLocalDate +import de.gematik.ti.erp.app.utils.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive @@ -46,38 +50,39 @@ enum class TaskStatus { Failed; } -fun extractTaskIds( +data class TaskData( + val taskId: String, + val status: TaskStatus, + val lastModified: FhirTemporal? +) + +@Requirement( + "O.Source_2#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Sanitization is also done for all FHIR mapping." +) +fun extractActualTaskData( bundle: JsonElement -): Pair> { +): Pair> { val bundleTotal = bundle.containedArrayOrNull("entry")?.size ?: 0 val resources = bundle .findAll("entry.resource") - val taskIds = resources.mapNotNull { resource -> + val tasks = resources.mapNotNull { resource -> val profileString = resource .contained("meta") .contained("profile") .contained() val status = mapTaskStatus(resource.containedString("status")) - when { - profileString.isProfileValue( - "https://gematik.de/fhir/StructureDefinition/ErxTask", - "1.1.1" - ) && status != TaskStatus.Canceled -> - resource - .findAll("identifier") - .filterWith( - "system", - stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID") - ) - .first() - .containedString("value") - + val lastModified = resource.contained("lastModified").jsonPrimitive.toFhirTemporal() + val taskId = when { profileString.isProfileValue( "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task", - "1.2" - ) && status != TaskStatus.Canceled -> + "1.2", + "1.3", + "1.4" + ) -> resource .findAll("identifier") .filterWith( @@ -89,9 +94,11 @@ fun extractTaskIds( else -> null } + + taskId?.let { TaskData(it, status, lastModified) } } - return bundleTotal to taskIds.toList() + return bundleTotal to tasks.toList() } fun extractTaskAndKBVBundle( @@ -114,12 +121,12 @@ fun extractTaskAndKBVBundle( .contained() when { + // TODO: remove Version 1.2 and 1.3 for GEM_ERP_PR_Task after 15.Jul.2025 profileString.isProfileValue( - "https://gematik.de/fhir/StructureDefinition/ErxTask", - "1.1.1" - ) || profileString.isProfileValue( "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task", - "1.2" + "1.2", + "1.3", + "1.4" ) -> { task = resource } @@ -143,103 +150,13 @@ fun extractTask( task: JsonElement, process: ( taskId: String, - accessCode: String?, + accessCode: String, lastModified: FhirTemporal.Instant, expiresOn: FhirTemporal.LocalDate?, acceptUntil: FhirTemporal.LocalDate?, authoredOn: FhirTemporal.Instant, - status: TaskStatus - ) -> Unit -) { - val profileString = task - .contained("meta") - .contained("profile") - .contained() - - when { - profileString.isProfileValue( - "https://gematik.de/fhir/StructureDefinition/ErxTask", - "1.1.1" - ) -> { - extractTaskVersion111(task, process) - } - - profileString.isProfileValue( - "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task", - "1.2" - ) -> { - extractTaskVersion12(task, process) - } - } -} - -fun extractTaskVersion111( - task: JsonElement, - process: ( - taskId: String, - accessCode: String?, - lastModified: FhirTemporal.Instant, - expiresOn: FhirTemporal.LocalDate?, - acceptUntil: FhirTemporal.LocalDate?, - authoredOn: FhirTemporal.Instant, - status: TaskStatus - ) -> Unit -) { - val taskId = task - .findAll("identifier") - .filterWith("system", stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID")) - .first() - .containedString("value") - - val accessCode = task - .findAll("identifier") - .filterWith("system", stringValue("https://gematik.de/fhir/NamingSystem/AccessCode")) - .firstOrNull() - ?.containedString("value") - - val status = mapTaskStatus(task.containedString("status")) - val authoredOn = requireNotNull(task.contained("authoredOn").jsonPrimitive.asFhirInstant()) { - "Couldn't parse `authoredOn`" - } - val lastModified = requireNotNull(task.contained("lastModified").jsonPrimitive.asFhirInstant()) { - "Couldn't parse `lastModified`" - } - - val expiresOn = task - .findAll("extension") - .filterWith("url", stringValue("https://gematik.de/fhir/StructureDefinition/ExpiryDate")) - .first() - .contained("valueDate") - .jsonPrimitive.asFhirLocalDate() - - val acceptUntil = task - .findAll("extension") - .filterWith("url", stringValue("https://gematik.de/fhir/StructureDefinition/AcceptDate")) - .first() - .contained("valueDate") - .jsonPrimitive.asFhirLocalDate() - - process( - taskId, - accessCode, - lastModified, - expiresOn, - acceptUntil, - authoredOn, - status - ) -} - -fun extractTaskVersion12( - task: JsonElement, - process: ( - taskId: String, - accessCode: String?, - lastModified: FhirTemporal.Instant, - expiresOn: FhirTemporal.LocalDate?, - acceptUntil: FhirTemporal.LocalDate?, - authoredOn: FhirTemporal.Instant, - status: TaskStatus + status: TaskStatus, + lastMedicationDispense: FhirTemporal.Instant? ) -> Unit ) { val taskId = task @@ -252,7 +169,7 @@ fun extractTaskVersion12( .findAll("identifier") .filterWith("system", stringValue("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode")) .firstOrNull() - ?.containedString("value") + ?.containedStringOrNull("value") ?: "" // 169 and 209 direct assignments have not an access code val status = mapTaskStatus(task.containedString("status")) @@ -277,6 +194,18 @@ fun extractTaskVersion12( .contained("valueDate") .jsonPrimitive.asFhirLocalDate() + val lastMedicationDispense = task + .findAll("extension") + .filterWith( + "url", + stringValue( + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_LastMedicationDispense" + ) + ) + .firstOrNull() + ?.containedOrNull("valueInstant") + ?.jsonPrimitive?.asFhirInstant() + process( taskId, accessCode, @@ -284,7 +213,8 @@ fun extractTaskVersion12( expiresOn, acceptUntil, authoredOn, - status + status, + lastMedicationDispense ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Comperator.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Comperator.kt index 4417a79e..5af19300 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Comperator.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Comperator.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt index e82b7a74..56ea49f8 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("TooManyFunctions") @@ -122,6 +122,9 @@ fun JsonElement.containedOrNull(key: String) = fun JsonElement.containedObject(key: String) = this.contained(key).containedObject() +fun JsonElement.containedObjectOrNull(key: String) = + this.containedOrNull(key)?.containedObjectOrNull() + fun JsonElement.containedArray(key: String) = this.contained(key).containedArray() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Formatter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Formatter.kt index 57e1a5ee..659abae3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Formatter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Formatter.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Parser.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Parser.kt index 020c2375..afdc61a0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Parser.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Parser.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt index 5070186c..d452581c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt index c76846c6..a7d309ec 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt index ebc0eff8..acb80162 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.idp import org.jose4j.jws.EcdsaUsingShaAlgorithm diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt index 30656f35..bc8dc33b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") +@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping", "MandatoryBracesIfStatements", "MagicNumber") package de.gematik.ti.erp.app.idp @@ -33,11 +33,20 @@ import java.security.spec.ECPoint import java.security.spec.EllipticCurve @Requirement( - "GS-A_4357-2#2", - "GS-A_4361-2#2", + "A_17207#1", sourceSpecification = "gemSpec_Krypt", rationale = "Support for required algorithms implemented using ECDSA." ) +@Requirement( + "GS-A_4357-02#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "Support for required algorithms implemented using ECDSA." +) +@Requirement( + "A_21332-02#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "The brainpool curves (P-256 oder P-384) for signature checking." +) object EllipticCurvesExtending : EllipticCurves() { const val BP_256 = "BP-256" const val BP_384 = "BP-384" diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt index 3329454a..f48790ee 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.idp import de.gematik.ti.erp.app.Requirement @@ -33,9 +35,10 @@ import java.security.Signature import javax.crypto.SecretKey @Requirement( - "A_21324", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Generate Key-Verifier using Token-Key and Code-Verifier." + "A_21324#1", + "A_21323#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Encrypting the key verifier with a JSON Web Encryption (JWE)." ) fun buildKeyVerifier( tokenKey: SecretKey, @@ -83,9 +86,13 @@ suspend fun buildJsonWebSignatureWithHealthCard( @Requirement( "O.Cryp_1#5", - "O.Cryp_4#5", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature via ecdh ephemeral-static [one time usage]" +) +@Requirement( + "O.Cryp_4#7", + sourceSpecification = "BSI-eRp-ePA", + rationale = "One time usage for JWE ECDH-ES Encryption" ) fun buildJsonWebSignatureWithSecureElement( builder: JsonWebSignature.() -> Unit, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt index 02fa036c..52cbce6e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.api @@ -42,23 +42,18 @@ import java.net.URI private const val CODE_CHALLENGE_METHOD = "S256" private const val RESPONSE_CODE = "code" -@Requirement( - "A_20603#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Store the client-id registered with the IDP and add it to requests." -) const val CLIENT_ID = "eRezeptApp" @Requirement( "A_20740#1", - sourceSpecification = "gemSpec_eRp_FdV", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Store the redirect URI for this app." ) const val REDIRECT_URI = "https://redirect.gematik.de/erezept" const val EXT_AUTH_REDIRECT_URI: String = "https://das-e-rezept-fuer-deutschland.de/extauth" @Requirement( - "O.Purp_8#1", + "O.Purp_8#2", sourceSpecification = "BSI-eRp-ePA", rationale = "Interface of external idp service" ) @@ -92,11 +87,6 @@ interface IdpService { @Query("code_challenge") codeChallenge: String ): Response - @Requirement( - "A_20603#3", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Store the client-id registered with the IDP and add it to requests." - ) @GET suspend fun fetchTokenChallenge( @Url url: String, @@ -119,9 +109,10 @@ interface IdpService { ): Response @Requirement( - "A_20603#4", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Store the client-id registered with the IDP and add it to requests." + "A_20529-01#2", + "A_20483#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Sending encrypted KEY_VERIFIER and AUTHORIZATION_CODE." ) @FormUrlEncoded @POST diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/AuthenticationData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/AuthenticationData.kt index c9667675..524eef99 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/AuthenticationData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/AuthenticationData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.api.models diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt index f1427b36..8995448a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt @@ -1,25 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:UseSerializers(JWSSerializer::class) +@file:Suppress("MagicNumber") package de.gematik.ti.erp.app.idp.api.models +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.secureRandomInstance import kotlinx.datetime.Instant @@ -54,6 +56,11 @@ data class RemoteFastTrackIdp( @SerialName("kk_app_id") val id: String ) +@Requirement( + "A_22302-01#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Data class for Forwarding the Authorization Code to the IDP service." +) @Serializable data class RemoteFederationIdp( @SerialName("idp_name") val name: String, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt index 505e32ac..9182a712 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.api.models diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/UniversalLinkToken.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/UniversalLinkToken.kt index f547a6ea..8c6c3881 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/UniversalLinkToken.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/api/models/UniversalLinkToken.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.api.models @@ -53,7 +53,7 @@ data class UniversalLinkToken( } companion object { - private fun URI.toUniversalLinkToken(): UniversalLinkToken? { + fun URI.toUniversalLinkToken(): UniversalLinkToken? { val entries = getQueryPairs() ((entries findIdentifierFor Code) to (entries findIdentifierFor State)) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/extension/UriExtension.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/extension/UriExtension.kt index 75f36654..13da0384 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/extension/UriExtension.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/extension/UriExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.extension diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/HealthInsuranceData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/HealthInsuranceData.kt index f8ab9fba..1577c7b2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/HealthInsuranceData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/HealthInsuranceData.kt @@ -1,26 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model -import de.gematik.ti.erp.app.idp.api.models.RemoteFastTrackIdp import de.gematik.ti.erp.app.idp.api.models.RemoteFederationIdp - data class HealthInsuranceData( val name: String, val id: String, @@ -37,13 +35,5 @@ data class HealthInsuranceData( isGid = isGid, logo = logo ) - - fun RemoteFastTrackIdp.mapToDomain() = - HealthInsuranceData( - name = name, - id = id, - isGid = false, - logo = null - ) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt index 423fd14c..a8adb5d0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt @@ -1,24 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model -import de.gematik.ti.erp.app.Requirement import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder @@ -57,11 +56,7 @@ object IdpData { val token: SingleSignOnToken? } - @Requirement( - "A_21595#1", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Data structure holding the health card certificate." - ) + // A_21595 (Data structure holding the health card certificate.) sealed interface TokenWithHealthCardScope : SingleSignOnTokenScope { val cardAccessNumber: String val healthCardCertificate: X509CertificateHolder @@ -74,11 +69,7 @@ object IdpData { Base64Url.encode(aliasOfSecureElementEntry) // url safe for compatibility with response from idp backend } - @Requirement( - "A_21595#2", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Data structure holding the health card certificate." - ) + // A_21595 (Data structure holding the health card certificate) data class DefaultToken( override val token: SingleSignOnToken?, override val cardAccessNumber: String, @@ -101,11 +92,7 @@ object IdpData { val authenticatorName: String ) : SingleSignOnTokenScope - @Requirement( - "A_21595#3", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Data structure holding the health card certificate." - ) + // A_21595 (Data structure holding the health card certificate) data class AlternateAuthenticationToken( override val token: SingleSignOnToken?, override val cardAccessNumber: String, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/UniversalLinkIdp.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/UniversalLinkIdp.kt index c56a27d5..ac74a126 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/UniversalLinkIdp.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/UniversalLinkIdp.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/GematikResponseError.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/GematikResponseError.kt index 9abd5e61..64af7075 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/GematikResponseError.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/GematikResponseError.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model.error diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/ResponseError.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/ResponseError.kt index 96aaf409..b5472c33 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/ResponseError.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/ResponseError.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model.error diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/SingleSignOnTokenError.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/SingleSignOnTokenError.kt index b138c311..db9a5706 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/SingleSignOnTokenError.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/SingleSignOnTokenError.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model.error diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/UniversalLinkError.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/UniversalLinkError.kt index a76efefd..5621fc92 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/UniversalLinkError.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/error/UniversalLinkError.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.model.error diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/AccessTokenDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/AccessTokenDataSource.kt index d75c30bf..417eb9ac 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/AccessTokenDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/AccessTokenDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.repository @@ -36,7 +36,7 @@ class AccessTokenDataSource { @Requirement( "A_21328#1", - sourceSpecification = "gemSpec_eRp_FdV", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Store access token in data structure only." ) private val decryptedAccessTokenMap: MutableStateFlow> = diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/DefaultIdpRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/DefaultIdpRepository.kt new file mode 100644 index 00000000..1ef80391 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/DefaultIdpRepository.kt @@ -0,0 +1,344 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.idp.repository + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.idp.api.models.Challenge +import de.gematik.ti.erp.app.idp.api.models.IdpDiscoveryInfo +import de.gematik.ti.erp.app.idp.api.models.IdpNonce +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.idp.api.models.IdpState +import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey +import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntries +import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry +import de.gematik.ti.erp.app.idp.api.models.RemoteFederationIdp +import de.gematik.ti.erp.app.idp.api.models.RemoteFederationIdps +import de.gematik.ti.erp.app.idp.api.models.TokenResponse +import de.gematik.ti.erp.app.idp.api.models.UniversalLinkToken +import de.gematik.ti.erp.app.idp.extension.extractNullableQueryParameter +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.model.error.GematikResponseError +import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.parseToError +import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.parsedUriToError +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.vau.extractECPublicKey +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant +import kotlinx.serialization.json.Json +import org.bouncycastle.cert.X509CertificateHolder +import org.jose4j.base64url.Base64 +import org.jose4j.jws.JsonWebSignature +import java.net.HttpURLConnection +import java.net.URI +import java.security.PublicKey + +class DefaultIdpRepository( + private val remoteDataSource: IdpRemoteDataSource, + private val localDataSource: IdpLocalDataSource, + private val accessTokenDataSource: AccessTokenDataSource +) : IdpRepository { + private val json = Json { ignoreUnknownKeys = true } + + override fun decryptedAccessToken(profileId: ProfileIdentifier): Flow = accessTokenDataSource.get(profileId) + + @Requirement( + "A_21328#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Store access token in data structure only." + ) + @Requirement( + "O.Auth_13#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Store access token in data structure only." + ) + override fun saveDecryptedAccessToken(profileId: ProfileIdentifier, accessToken: AccessToken) { + accessTokenDataSource.save(profileId, accessToken) + } + + @Requirement( + "O.Auth_13#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Store SSO token to encrypted local data source." + ) + override suspend fun saveSingleSignOnToken(profileId: ProfileIdentifier, token: IdpData.SingleSignOnTokenScope) { + localDataSource.saveSingleSignOnToken(profileId, token) + } + + override fun authenticationData(profileId: ProfileIdentifier): Flow = + localDataSource.authenticationData(profileId) + + override suspend fun fetchChallenge( + url: String, + codeChallenge: String, + state: String, + nonce: String, + isDeviceRegistration: Boolean, + redirectUri: String + ): Result = + remoteDataSource.fetchChallenge( + url = url, + codeChallenge = codeChallenge, + state = state, + nonce = nonce, + isDeviceRegistration = isDeviceRegistration, + redirectUri = redirectUri + ) + + @Requirement( + "A_20741#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Discovery document is downloaded." + ) + /** + * Returns an unchecked and possible invalid idp configuration parsed from the discovery document. + */ + override suspend fun loadUncheckedIdpConfiguration(): IdpData.IdpConfiguration { + return localDataSource.loadIdpInfo() ?: run { + extractUncheckedIdpConfiguration( + remoteDataSource.fetchDiscoveryDocument().getOrThrow() + ).also { localDataSource.saveIdpInfo(it) } + } + } + + override suspend fun postSignedChallenge(url: String, signedChallenge: String): Result = + remoteDataSource.postChallenge(url, signedChallenge) + + override suspend fun postUnsignedChallengeWithSso( + url: String, + ssoToken: String, + unsignedChallenge: String + ): Result = + remoteDataSource.postChallenge(url, ssoToken, unsignedChallenge) + + override suspend fun postToken( + url: String, + keyVerifier: String, + code: String, + redirectUri: String + ): Result = + remoteDataSource.postToken( + url, + keyVerifier = keyVerifier, + code = code, + redirectUri = redirectUri + ) + + override suspend fun fetchFederationIDList( + url: String, + idpPukSigKey: PublicKey + ): List { + val jwtResult = remoteDataSource.fetchExternalAuthorizationIDList(url).getOrThrow() + return extractFederationIdpList(jwtResult.apply { key = idpPukSigKey }.payload) + } + + override suspend fun fetchIdpPukSig(url: String): Result = + remoteDataSource.fetchIdpPukSig(url) + + override suspend fun fetchIdpPukEnc(url: String): Result = + remoteDataSource.fetchIdpPukEnc(url) + + private fun parseDiscoveryDocumentBody(body: String): IdpDiscoveryInfo = + json.decodeFromString(body) + + private fun extractFederationIdpList(payload: String): List { + return json.decodeFromString(payload).items + } + + private fun extractUncheckedIdpConfiguration(discoveryDocument: JWSDiscoveryDocument): IdpData.IdpConfiguration { + val x5c = requireNotNull( + (discoveryDocument.jws.headers?.getObjectHeaderValue("x5c") as? ArrayList<*>)?.firstOrNull() as? String + ) { "Missing certificate" } + val certificateHolder = X509CertificateHolder(Base64.decode(x5c)) + + discoveryDocument.jws.key = certificateHolder.extractECPublicKey() + + val discoveryDocumentBody = parseDiscoveryDocumentBody(discoveryDocument.jws.payload) + + return IdpData.IdpConfiguration( + authorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.authorizationUrl), + ssoEndpoint = overwriteEndpoint(discoveryDocumentBody.ssoUrl), + tokenEndpoint = overwriteEndpoint(discoveryDocumentBody.tokenUrl), + pairingEndpoint = discoveryDocumentBody.pairingUrl, + authenticationEndpoint = overwriteEndpoint(discoveryDocumentBody.authenticationUrl), + pukIdpEncEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpEnc), + pukIdpSigEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpSig), + expirationTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.expirationTime), + issueTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.issuedAt), + certificate = certificateHolder, + externalAuthorizationIDsEndpoint = overwriteEndpoint(discoveryDocumentBody.healthInsuranceAppV1Url), + thirdPartyAuthorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.thirdPartyAuthorizationV1Url), + federationAuthorizationIDsEndpoint = overwriteEndpoint(discoveryDocumentBody.healthInsuranceAppV2Url), + federationAuthorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.thirdPartyAuthorizationV2Url) + ) + } + + private fun overwriteEndpoint(oldEndpoint: String?) = + oldEndpoint?.replace(".zentral.idp.splitdns.ti-dienste.de", ".app.ti-dienste.de") ?: "" + + override suspend fun postPairing( + url: String, + encryptedRegistrationData: String, + token: String + ): Result = + remoteDataSource.postPairing( + url, + token = token, + encryptedRegistrationData = encryptedRegistrationData + ) + + override suspend fun getPairing( + url: String, + token: String + ): Result = + remoteDataSource.getPairing( + url, + token = token + ) + + override suspend fun deletePairing( + url: String, + token: String, + alias: String + ): Result = + remoteDataSource.deletePairing( + url = url, + token = token, + alias = alias + ) + + override suspend fun postBiometricAuthenticationData( + url: String, + encryptedSignedAuthenticationData: String + ): Result = + remoteDataSource.authorizeBiometric(url, encryptedSignedAuthenticationData) + + override suspend fun authorizeExternalHealthInsuranceAppDataAsGiD( + url: String, + token: UniversalLinkToken + ): Result = + remoteDataSource.authorizeExternalAppDataWithGid( + url = url, + code = token.code, + state = token.state + ) + + // A_21603 Invalidate/delete session data upon logout. + // since we have automatic memory management, we can't delete the token. + // Due to the use of frameworks we have sensitive data as immutable objects and hence cannot override it + @Requirement( + "A_21326#2", + "A_21327#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Invalidate, delete session data on logout" + ) + @Requirement( + "O.Source_5#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Invalidate/delete access token and configuration data.", + codeLines = 4 + ) + override suspend fun invalidate(profileId: ProfileIdentifier) { + invalidateConfig() + invalidateDecryptedAccessToken(profileId) + localDataSource.invalidateAuthenticationData(profileId) + } + + override suspend fun invalidateConfig() { + localDataSource.invalidateConfiguration() + } + + @Requirement( + "O.Source_5#1", + "O.Auth_14#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Invalidates the single sign on token and delete the access token.", + codeLines = 4 + ) + override suspend fun invalidateSingleSignOnTokenRetainingScope(profileId: ProfileIdentifier) { + localDataSource.invalidateSingleSignOnTokenRetainingScope(profileId) + invalidateDecryptedAccessToken(profileId) + } + + @Requirement( + "O.Source_5#2", + "O.Auth_14#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Delete unauthorized access token.", + codeLines = 4 + ) + override fun invalidateDecryptedAccessToken(profileId: ProfileIdentifier) { + accessTokenDataSource.delete(profileId) + } + + /** + * @return URI as a result object + * @throws GematikResponseError as a result object + * @param url authentication url + * @param state initial state + * @param codeChallenge initial code challenge + * @param nonce initial nonce + * @param externalAppId universalLinkIdp authenticatorId + * @param idpScope default scope + */ + override suspend fun getGidAuthorizationRedirect( + url: String, + state: IdpState, + codeChallenge: String, + nonce: IdpNonce, + externalAppId: String, + idpScope: IdpScope + ): Result { + try { + val response = remoteDataSource.getGidAuthorizationRedirectUrl( + url = url, + externalAppId = externalAppId, + codeChallenge = codeChallenge, + nonce = nonce.nonce, + state = state.state, + isPairingScope = idpScope == IdpScope.BiometricPairing + ) + if (response.code() == HttpURLConnection.HTTP_MOVED_TEMP) { + val headers = response.headers() + val redirectUri = requireNotNull(headers["Location"]) { + "Missing parameters to get redirect uri" + } + val parsedUri = URI(redirectUri) + return if (parsedUri.extractNullableQueryParameter("error") != null) { + val error = parsedUri.parsedUriToError() + Napier.e { "error on gid response ${error.gematikErrorText}" } + Result.failure(error) + } else { + Napier.d { "success on gid response $parsedUri" } + Result.success(parsedUri) + } + } else { + Napier.e { "error on gid response, wrong response code ${response.code()}" } + val error = response.body().toString() + return Result.failure(error.parseToError()) + } + } catch (e: Throwable) { + Napier.e { "failure on gid response ${e.message}" } + return Result.failure(GematikResponseError.emptyResponseError(e.message)) + } + } +} + +@JvmInline +value class JWSDiscoveryDocument(val jws: JsonWebSignature) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt index 22d5a14b..3589cae6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.repository @@ -38,9 +38,12 @@ import kotlinx.coroutines.flow.map import org.bouncycastle.cert.X509CertificateHolder import java.security.KeyStore -class IdpLocalDataSource constructor( - private val realm: Realm -) { +@Requirement( + "O.Data_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The encrypted realm database is used to store data." +) +class IdpLocalDataSource(private val realm: Realm) { suspend fun saveIdpInfo(config: IdpData.IdpConfiguration) { realm.writeOrCopyToRealm(::IdpConfigurationEntityV1) { entity -> entity.authorizationEndpoint = config.authorizationEndpoint @@ -86,18 +89,15 @@ class IdpLocalDataSource constructor( } } + // "A_21322" + // A_21595 (Save the SSO token to database) + // "A_21322" @Requirement( "A_21328#2", - "A_21322", - "A_21595#4", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Save the SSO token to database." - ) - @Requirement( - "O.Tokn_1#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Save the SSO token to realm database." + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Save the SSO token to database that is encrypted." ) + @Suppress("CyclomaticComplexMethod") suspend fun saveSingleSignOnToken( profileId: ProfileIdentifier, tokenScope: IdpData.SingleSignOnTokenScope @@ -153,12 +153,6 @@ class IdpLocalDataSource constructor( } } } - - @Requirement( - "O.Tokn_6#5", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate authentication data from keystore " - ) suspend fun invalidateAuthenticationData(profileId: ProfileIdentifier) { writeToRealm(profileId) { profile -> getOrInsertAuthData(profile)?.apply { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpPairingRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpPairingRepository.kt index fcd8494f..a0b4681d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpPairingRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpPairingRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.repository @@ -38,11 +38,6 @@ class IdpPairingRepository constructor( fun decryptedAccessToken(profileId: ProfileIdentifier) = decryptedAccessTokenMap.map { it[profileId] }.distinctUntilChanged() - @Requirement( - "O.Tokn_1#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Save the access token token to mutable state." - ) fun saveDecryptedAccessToken(profileId: ProfileIdentifier, accessToken: AccessToken) { decryptedAccessTokenMap.update { it + (profileId to accessToken) @@ -51,6 +46,7 @@ class IdpPairingRepository constructor( @Requirement( "A_21326#1", + "A_21327#1", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "removing decrypted access token from map" + "since we have automatic memory management, we can't delete the token. " + diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt index a7c543bd..2e61300c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("TopLevelPropertyNaming") + package de.gematik.ti.erp.app.idp.repository import de.gematik.ti.erp.app.api.ApiCallException diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt index 6380a588..a0798034 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt @@ -1,26 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.repository -import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.idp.api.models.Challenge -import de.gematik.ti.erp.app.idp.api.models.IdpDiscoveryInfo import de.gematik.ti.erp.app.idp.api.models.IdpNonce import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.idp.api.models.IdpState @@ -28,57 +26,19 @@ import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntries import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry import de.gematik.ti.erp.app.idp.api.models.RemoteFederationIdp -import de.gematik.ti.erp.app.idp.api.models.RemoteFederationIdps import de.gematik.ti.erp.app.idp.api.models.TokenResponse import de.gematik.ti.erp.app.idp.api.models.UniversalLinkToken -import de.gematik.ti.erp.app.idp.extension.extractNullableQueryParameter import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.idp.model.error.GematikResponseError -import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.parseToError -import de.gematik.ti.erp.app.idp.model.error.GematikResponseError.Companion.parsedUriToError import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.vau.extractECPublicKey -import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow -import kotlinx.datetime.Instant -import kotlinx.serialization.json.Json -import org.bouncycastle.cert.X509CertificateHolder -import org.jose4j.base64url.Base64 -import org.jose4j.jws.JsonWebSignature -import java.net.HttpURLConnection import java.net.URI import java.security.PublicKey -class IdpRepository( - private val remoteDataSource: IdpRemoteDataSource, - private val localDataSource: IdpLocalDataSource, - private val accessTokenDataSource: AccessTokenDataSource -) { - private val json = Json { ignoreUnknownKeys = true } - - fun decryptedAccessToken(profileId: ProfileIdentifier) = accessTokenDataSource.get(profileId) - - @Requirement( - "A_21328#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Store access token in data structure only." - ) - fun saveDecryptedAccessToken(profileId: ProfileIdentifier, accessToken: AccessToken) { - accessTokenDataSource.save(profileId, accessToken) - } - - suspend fun saveSingleSignOnToken(profileId: ProfileIdentifier, token: IdpData.SingleSignOnTokenScope) { - localDataSource.saveSingleSignOnToken(profileId, token) - } - - fun authenticationData(profileId: ProfileIdentifier): Flow = - localDataSource.authenticationData(profileId) - - @Requirement( - "A_20483", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Fetch challenge from IDP." - ) +interface IdpRepository { + fun decryptedAccessToken(profileId: ProfileIdentifier): Flow + fun authenticationData(profileId: ProfileIdentifier): Flow + fun saveDecryptedAccessToken(profileId: ProfileIdentifier, accessToken: AccessToken) + suspend fun saveSingleSignOnToken(profileId: ProfileIdentifier, token: IdpData.SingleSignOnTokenScope) suspend fun fetchChallenge( url: String, codeChallenge: String, @@ -86,195 +46,24 @@ class IdpRepository( nonce: String, isDeviceRegistration: Boolean, redirectUri: String - ): Result = - remoteDataSource.fetchChallenge( - url = url, - codeChallenge = codeChallenge, - state = state, - nonce = nonce, - isDeviceRegistration = isDeviceRegistration, - redirectUri = redirectUri - ) - - /** - * Returns an unchecked and possible invalid idp configuration parsed from the discovery document. - */ - suspend fun loadUncheckedIdpConfiguration(): IdpData.IdpConfiguration { - return localDataSource.loadIdpInfo() ?: run { - extractUncheckedIdpConfiguration( - remoteDataSource.fetchDiscoveryDocument().getOrThrow() - ).also { localDataSource.saveIdpInfo(it) } - } - } - - suspend fun postSignedChallenge(url: String, signedChallenge: String): Result = - remoteDataSource.postChallenge(url, signedChallenge) - - suspend fun postUnsignedChallengeWithSso( - url: String, - ssoToken: String, - unsignedChallenge: String - ): Result = - remoteDataSource.postChallenge(url, ssoToken, unsignedChallenge) - - suspend fun postToken( - url: String, - keyVerifier: String, - code: String, - redirectUri: String - ): Result = - remoteDataSource.postToken( - url, - keyVerifier = keyVerifier, - code = code, - redirectUri = redirectUri - ) - - suspend fun fetchFederationIDList( - url: String, - idpPukSigKey: PublicKey - ): List { - val jwtResult = remoteDataSource.fetchExternalAuthorizationIDList(url).getOrThrow() - return extractFederationIdpList(jwtResult.apply { key = idpPukSigKey }.payload) - } - - suspend fun fetchIdpPukSig(url: String): Result = - remoteDataSource.fetchIdpPukSig(url) - - suspend fun fetchIdpPukEnc(url: String): Result = - remoteDataSource.fetchIdpPukEnc(url) - - private fun parseDiscoveryDocumentBody(body: String): IdpDiscoveryInfo = - json.decodeFromString(body) - - private fun extractFederationIdpList(payload: String): List { - return json.decodeFromString(payload).items - } - - private fun extractUncheckedIdpConfiguration(discoveryDocument: JWSDiscoveryDocument): IdpData.IdpConfiguration { - val x5c = requireNotNull( - (discoveryDocument.jws.headers?.getObjectHeaderValue("x5c") as? ArrayList<*>)?.firstOrNull() as? String - ) { "Missing certificate" } - val certificateHolder = X509CertificateHolder(Base64.decode(x5c)) - - discoveryDocument.jws.key = certificateHolder.extractECPublicKey() - - val discoveryDocumentBody = parseDiscoveryDocumentBody(discoveryDocument.jws.payload) - - return IdpData.IdpConfiguration( - authorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.authorizationUrl), - ssoEndpoint = overwriteEndpoint(discoveryDocumentBody.ssoUrl), - tokenEndpoint = overwriteEndpoint(discoveryDocumentBody.tokenUrl), - pairingEndpoint = discoveryDocumentBody.pairingUrl, - authenticationEndpoint = overwriteEndpoint(discoveryDocumentBody.authenticationUrl), - pukIdpEncEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpEnc), - pukIdpSigEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpSig), - expirationTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.expirationTime), - issueTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.issuedAt), - certificate = certificateHolder, - externalAuthorizationIDsEndpoint = overwriteEndpoint(discoveryDocumentBody.healthInsuranceAppV1Url), - thirdPartyAuthorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.thirdPartyAuthorizationV1Url), - federationAuthorizationIDsEndpoint = overwriteEndpoint(discoveryDocumentBody.healthInsuranceAppV2Url), - federationAuthorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.thirdPartyAuthorizationV2Url) - ) - } - - private fun overwriteEndpoint(oldEndpoint: String?) = - oldEndpoint?.replace(".zentral.idp.splitdns.ti-dienste.de", ".app.ti-dienste.de") ?: "" - - suspend fun postPairing( - url: String, - encryptedRegistrationData: String, - token: String - ): Result = - remoteDataSource.postPairing( - url, - token = token, - encryptedRegistrationData = encryptedRegistrationData - ) - - suspend fun getPairing( - url: String, - token: String - ): Result = - remoteDataSource.getPairing( - url, - token = token - ) - - suspend fun deletePairing( - url: String, - token: String, - alias: String - ): Result = - remoteDataSource.deletePairing( - url = url, - token = token, - alias = alias - ) - - suspend fun postBiometricAuthenticationData( - url: String, - encryptedSignedAuthenticationData: String - ): Result = - remoteDataSource.authorizeBiometric(url, encryptedSignedAuthenticationData) - - suspend fun authorizeExternalHealthInsuranceAppDataAsGiD( - url: String, - token: UniversalLinkToken - ): Result = - remoteDataSource.authorizeExternalAppDataWithGid( - url = url, - code = token.code, - state = token.state - ) - - @Requirement( - "A_20186", - "A_21326", - "A_21327", - "A_20499-01", - "A_21603", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Invalidate/delete session data upon logout. " + - "since we have automatic memory management, we can't delete the token. " + - "Due to the use of frameworks we have sensitive data as immutable objects and hence " + - "cannot override it" - ) - @Requirement( - "O.Tokn_6#4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate config and token " - ) - suspend fun invalidate(profileId: ProfileIdentifier) { - invalidateConfig() - invalidateDecryptedAccessToken(profileId) - localDataSource.invalidateAuthenticationData(profileId) - } - - suspend fun invalidateConfig() { - localDataSource.invalidateConfiguration() - } - - suspend fun invalidateSingleSignOnTokenRetainingScope(profileId: ProfileIdentifier) { - localDataSource.invalidateSingleSignOnTokenRetainingScope(profileId) - invalidateDecryptedAccessToken(profileId) - } - - fun invalidateDecryptedAccessToken(profileId: ProfileIdentifier) { - accessTokenDataSource.delete(profileId) - } - - /** - * @return URI as a result object - * @throws GematikResponseError as a result object - * @param url authentication url - * @param state initial state - * @param codeChallenge initial code challenge - * @param nonce initial nonce - * @param externalAppId universalLinkIdp authenticatorId - * @param idpScope default scope - */ + ): Result + + suspend fun loadUncheckedIdpConfiguration(): IdpData.IdpConfiguration + suspend fun postSignedChallenge(url: String, signedChallenge: String): Result + suspend fun postUnsignedChallengeWithSso(url: String, ssoToken: String, unsignedChallenge: String): Result + suspend fun postToken(url: String, keyVerifier: String, code: String, redirectUri: String): Result + suspend fun fetchFederationIDList(url: String, idpPukSigKey: PublicKey): List + suspend fun fetchIdpPukSig(url: String): Result + suspend fun fetchIdpPukEnc(url: String): Result + suspend fun postPairing(url: String, encryptedRegistrationData: String, token: String): Result + suspend fun getPairing(url: String, token: String): Result + suspend fun deletePairing(url: String, token: String, alias: String): Result + suspend fun postBiometricAuthenticationData(url: String, encryptedSignedAuthenticationData: String): Result + suspend fun authorizeExternalHealthInsuranceAppDataAsGiD(url: String, token: UniversalLinkToken): Result + suspend fun invalidate(profileId: ProfileIdentifier) + suspend fun invalidateConfig() + suspend fun invalidateSingleSignOnTokenRetainingScope(profileId: ProfileIdentifier) + fun invalidateDecryptedAccessToken(profileId: ProfileIdentifier) suspend fun getGidAuthorizationRedirect( url: String, state: IdpState, @@ -282,41 +71,5 @@ class IdpRepository( nonce: IdpNonce, externalAppId: String, idpScope: IdpScope - ): Result { - try { - val response = remoteDataSource.getGidAuthorizationRedirectUrl( - url = url, - externalAppId = externalAppId, - codeChallenge = codeChallenge, - nonce = nonce.nonce, - state = state.state, - isPairingScope = idpScope == IdpScope.BiometricPairing - ) - if (response.code() == HttpURLConnection.HTTP_MOVED_TEMP) { - val headers = response.headers() - val redirectUri = requireNotNull(headers["Location"]) { - "Missing parameters to get redirect uri" - } - val parsedUri = URI(redirectUri) - return if (parsedUri.extractNullableQueryParameter("error") != null) { - val error = parsedUri.parsedUriToError() - Napier.e { "error on gid response ${error.gematikErrorText}" } - Result.failure(error) - } else { - Napier.d { "success on gid response $parsedUri" } - Result.success(parsedUri) - } - } else { - Napier.e { "error on gid response, wrong response code ${response.code()}" } - val error = response.body().toString() - return Result.failure(error.parseToError()) - } - } catch (e: Throwable) { - Napier.e { "failure on gid response ${e.message}" } - return Result.failure(GematikResponseError.emptyResponseError(e.message)) - } - } + ): Result } - -@JvmInline -value class JWSDiscoveryDocument(val jws: JsonWebSignature) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt index b6bffd56..71ec360f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AuthenticateWithExternalHealthInsuranceAppUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AuthenticateWithExternalHealthInsuranceAppUseCase.kt index 89a2abd4..3cd8544b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AuthenticateWithExternalHealthInsuranceAppUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AuthenticateWithExternalHealthInsuranceAppUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase @@ -43,23 +43,16 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import java.net.URI +// A_20601-01 @Requirement( - "A_20527#2", - "A_20600#2", - "A_20601", - "A_20601-01", - "A_22301", + "A_22301-01#1", sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "External authentication (Fast-Track / Gesundheit-Id)" -) -@Requirement( - "O.Plat_10#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Follow redirect" + rationale = "External authentication (Gesundheit-Id)" ) class AuthenticateWithExternalHealthInsuranceAppUseCase( private val idpRepository: IdpRepository, @@ -88,6 +81,11 @@ class AuthenticateWithExternalHealthInsuranceAppUseCase( val authorizationEndPoint = initialData.config.requiredAuthorizationEndPoint() + @Requirement( + "A_22301-01#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Acceptance of the Authorization Code via App2App communication." + ) idpRepository .authorizeExternalHealthInsuranceAppDataAsGiD( url = authorizationEndPoint, @@ -200,21 +198,24 @@ class AuthenticateWithExternalHealthInsuranceAppUseCase( "authorizationEndPoint null, Fast-track or Gid not available" } - private fun insurantIdentifier(idTokenJson: JsonElement?) = - idTokenJson?.jsonObject?.get("idNummer")?.jsonPrimitive?.content ?: "" + private fun insurantIdentifier(idTokenJson: JsonElement?): String = idTokenJson.getContent("idNummer") - private fun insuranceName(idTokenJson: JsonElement?) = - idTokenJson?.jsonObject?.get("organizationName")?.jsonPrimitive?.content - ?: "" + private fun insuranceName(idTokenJson: JsonElement?): String = idTokenJson.getContent("organizationName") + + private fun insurantName(jsonElement: JsonElement?): String { + val jsonObject = jsonElement?.jsonObject - private fun insurantName(idTokenJson: JsonElement?) = - idTokenJson?.jsonObject?.get("given_name")?.jsonPrimitive?.content - ?.let { - "$it ${ - idTokenJson.jsonObject["family_name"] - ?.jsonPrimitive?.content - }" - } ?: "" + // Extract fields safely + val displayName = jsonObject?.get("display_name")?.jsonPrimitive?.contentOrNull + val givenName = jsonObject?.get("given_name")?.jsonPrimitive?.contentOrNull + val familyName = jsonObject?.get("family_name")?.jsonPrimitive?.contentOrNull + + // Return the first non-null, non-empty value + return givenName?.takeIf { it.isNotEmpty() } + ?: familyName?.takeIf { it.isNotEmpty() } + ?: displayName?.takeIf { it.isNotEmpty() } + ?: "" + } private fun saveAccessToken( idpTokenResult: IdpTokenResult?, @@ -273,4 +274,7 @@ class AuthenticateWithExternalHealthInsuranceAppUseCase( throw SingleSignOnTokenError } } + + private fun (JsonElement?).getContent(elementName: String): String = + this?.jsonObject?.get(elementName)?.jsonPrimitive?.contentOrNull ?: "" } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ChooseAuthenticationDataUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ChooseAuthenticationDataUseCase.kt new file mode 100644 index 00000000..7f5cc812 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ChooseAuthenticationDataUseCase.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.idp.usecase + +import de.gematik.ti.erp.app.authentication.model.Biometric +import de.gematik.ti.erp.app.authentication.model.External +import de.gematik.ti.erp.app.authentication.model.HealthCard +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.None +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.validateRequirementForLastAuthUpdateRequired +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ChooseAuthenticationDataUseCase( + private val profileRepository: ProfileRepository, + private val idpRepository: IdpRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + profileId: ProfileIdentifier + ): Flow = + withContext(dispatcher) { + idpRepository.authenticationData(profileId) + .mapNotNull { idpAuthenticationData -> + val profile = profileRepository.getProfileById(profileId) + .first() + .toModel() + .validateRequirementForLastAuthUpdateRequired { id, lastAuthenticated -> + launch { profileRepository.updateLastAuthenticated(id, lastAuthenticated) } + } + val ssoTokenScope = idpAuthenticationData.singleSignOnTokenScope + + Napier.i(tag = "Authentication State", message = "ssoTokenScope for choosing authentication ${ssoTokenScope?.token?.token}") + + when (ssoTokenScope) { + is IdpData.ExternalAuthenticationToken -> External( + authenticatorId = ssoTokenScope.authenticatorId, + authenticatorName = ssoTokenScope.authenticatorName, + profile = profile + ) + + is IdpData.AlternateAuthenticationToken, + is IdpData.AlternateAuthenticationWithoutToken -> Biometric(profile = profile) + + is IdpData.DefaultToken -> HealthCard( + can = ssoTokenScope.cardAccessNumber, + profile = profile + ) + + null -> None(profile = profile) + } + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt index 82024f35..655b0e30 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("LongParameterList", "TooGenericExceptionCaught", "MagicNumber", "ThrowsCount") package de.gematik.ti.erp.app.idp.usecase @@ -41,7 +42,8 @@ import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.bouncycastle.util.encoders.Base64 -import java.net.HttpURLConnection +import java.net.HttpURLConnection.HTTP_FORBIDDEN +import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import java.security.KeyStore import java.security.PrivateKey import java.security.PublicKey @@ -62,9 +64,7 @@ class DefaultIdpUseCase( */ @Requirement( "A_20283-01#1", - "A_21326", - "A_21327", - sourceSpecification = "gemSpec_eRp_FdV", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Load and decrypt access token." ) override suspend fun loadAccessToken( @@ -117,6 +117,7 @@ class DefaultIdpUseCase( } } + @Suppress("CyclomaticComplexMethod", "NestedBlockDepth") private suspend fun loadAccessToken( refresh: Boolean = false, profileId: ProfileIdentifier, @@ -137,8 +138,9 @@ class DefaultIdpUseCase( savedAccessToken.expiresOn <= Clock.System.now() } - Napier.d { + Napier.d(tag = "IdpUseCase") { """Loading access token with: + |ssoTokenScope.present: ${ssoTokenScope != null} |refresh: $refresh |profileId: $profileId |scope: $scope @@ -158,41 +160,50 @@ class DefaultIdpUseCase( ) } + if (ssoTokenScope.token?.isValid() == false) { + Napier.e(tag = "IdpUseCase") { "expired SSO Token ${ssoTokenScope.token?.token}" } + } + if (refresh || accessToken == null || isExpired) { invalidateDecryptedAccessToken() - val actualToken = ssoTokenScope.token!!.token - - val initialData = try { - basicUseCase.initializeConfigurationAndKeys() - } catch (e: Exception) { - throw IDPConfigException(e) - } - try { - val refreshData = basicUseCase.refreshAccessTokenWithSsoFlow( - initialData, - scope = scope, - ssoToken = actualToken, - redirectUri = if (ssoTokenScope is IdpData.ExternalAuthenticationToken) { - EXT_AUTH_REDIRECT_URI - } else { - REDIRECT_URI - } - ) - saveDecryptedAccessToken(refreshData.accessToken, refreshData.expiresOn) - refreshData.accessToken - } catch (e: Exception) { - Napier.e("Couldn't refresh access token", e) - (e as? ApiCallException)?.also { - when (it.response.code()) { - // 400 returned by redirect call if sso token is not valid anymore - 400, 401, 403 -> { - invalidateSingleSignOnTokenRetainingScope() - throw RefreshFlowException(true, ssoTokenScope, e) + ssoTokenScope.token?.token?.let { actualToken -> + val initialData = try { + basicUseCase.initializeConfigurationAndKeys() + } catch (e: Exception) { + throw IDPConfigException(e) + } + try { + val refreshData = basicUseCase.refreshAccessTokenWithSsoFlow( + initialData, + scope = scope, + ssoToken = actualToken, + redirectUri = if (ssoTokenScope is IdpData.ExternalAuthenticationToken) { + EXT_AUTH_REDIRECT_URI + } else { + REDIRECT_URI + } + ) + saveDecryptedAccessToken(refreshData.accessToken, refreshData.expiresOn) + refreshData.accessToken + } catch (e: Exception) { + Napier.e(tag = "IdpUseCase", message = "Couldn't refresh access token", throwable = e) + (e as? ApiCallException)?.also { + when (it.response.code()) { + // 400 returned by redirect call if sso token is not valid anymore + 400, 401, 403 -> { + Napier.e(tag = "IdpUseCase") { "RefreshFlowException due to ApiCallException.code ${it.response.code()}" } + invalidateSingleSignOnTokenRetainingScope() + throw RefreshFlowException(true, ssoTokenScope, e) + } } } + throw RefreshFlowException(false, null, e) } - throw RefreshFlowException(false, null, e) + } ?: run { + Napier.e(tag = "IdpUseCase", message = "ssoTokenScope.token?.token is null!") + invalidateDecryptedAccessToken() + throw RefreshFlowException(false, null, NullPointerException("SSO token is null")) } } else { savedAccessToken.accessToken @@ -202,7 +213,7 @@ class DefaultIdpUseCase( throw RefreshFlowException( true, null, - "SSO token not set for $profileId!" + "SSO token-scope is null for $profileId!" ) } } @@ -210,13 +221,11 @@ class DefaultIdpUseCase( /** * Initial flow fetching the sso & access token requiring the health card to sign the challenge. */ + // // A_20601-01 @Requirement( - "A_20600#1", - "A_20601", - "A_20601-01", - "A_21598#2", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Authenticate to the IDP using the health card certificate." + "A_20167-02#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Authenticate to the IDP using the health card certificate" ) override suspend fun authenticationFlowWithHealthCard( profileId: ProfileIdentifier, @@ -297,6 +306,11 @@ class DefaultIdpUseCase( /** * Pairing flow fetching the sso & access token requiring the health card and generated key material. */ + @Requirement( + "A_20167-02#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Authenticate to the IDP using the health card certificate an secure element." + ) override suspend fun alternatePairingFlowWithSecureElement( profileId: ProfileIdentifier, cardAccessNumber: String, @@ -350,9 +364,9 @@ class DefaultIdpUseCase( * sets the sso & access token within the repository. */ @Requirement( - "A_21598#1", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Authentication flow with health card and secure element." + "A_20167-02#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Authenticate to the IDP using an secure element." ) override suspend fun alternateAuthenticationFlowWithSecureElement( profileId: ProfileIdentifier, @@ -406,35 +420,44 @@ class DefaultIdpUseCase( authData: IdpAuthFlowResult ) -> R ): R { - val ssoTokenScope = requireNotNull(repository.authenticationData(profileId).first().singleSignOnTokenScope) + val ssoTokenScope = requireNotNull(repository.authenticationData(profileId).first().singleSignOnTokenScope) // TODO: Throws IllegalStateException val authTokenScope = - requireNotNull(ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope) { "Wrong authentication scope!" } + requireNotNull(ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope) { "Wrong authentication scope!" } // TODO: Throws IllegalStateException val healthCardCertificate = authTokenScope.healthCardCertificate val aliasOfSecureElementEntry = authTokenScope.aliasOfSecureElementEntry + @Requirement( + "O.Cryp_7#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "java.security.PrivateKey used as the private-key container.", + codeLines = 2 + ) lateinit var privateKeyOfSecureElementEntry: PrivateKey lateinit var signatureObjectOfSecureElementEntry: Signature @Requirement( "O.Cryp_1#2", - "O.Cryp_4#2", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature via ecdh ephemeral-static [one time usage]", + codeLines = 30 ) @Requirement( - "O.Cryp_6", + "O.Cryp_4#3", sourceSpecification = "BSI-eRp-ePA", - rationale = "Persisted cryptographic keys are created within the devices key store. " + - "Temporal keys are discarded as soon as usage is no longer needed." + rationale = "One time usage for JWE ECDH-ES Encryption" ) @Requirement( - "O.Cryp_7", + "O.Cryp_6#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "As Brainpool256R1 is not available within key store but enforced by BSI where possible, " + - "we use secure enclave encryption only for biometric authentication. " + - "Everywhere else, cryptographic operations are ephemeral or use the eGK " + - "as a secure execution environment." + rationale = "Secure enclave key generation", + codeLines = 18 + ) + @Requirement( + "A_21590#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Key generation for authentication", + codeLines = 18 ) try { privateKeyOfSecureElementEntry = ( @@ -465,7 +488,6 @@ class DefaultIdpUseCase( privateKeyOfSecureElementEntry = privateKeyOfSecureElementEntry, signatureObjectOfSecureElementEntry = signatureObjectOfSecureElementEntry ) - return finally( initialData, authTokenScope, @@ -476,8 +498,8 @@ class DefaultIdpUseCase( /** * Returns the paired devices associated with the [profileId]s sso token scope. * - * @param authenticateWithSecureElement will be called if an alternate authentication is required. - * @param authenticateWithHealthCard will be called if a health card authentication is required + * [de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase.authenticateWithSecureElement] will be called if an alternate authentication is required. + * [de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase.authenticateWithHealthCard] will be called if a health card authentication is required * which needs to sign [hash]. */ override suspend fun getPairedDevices(profileId: ProfileIdentifier): @@ -489,6 +511,8 @@ class DefaultIdpUseCase( scope = IdpScope.BiometricPairing ) + Napier.e { "access token $accessToken" } + altAuthUseCase.getPairedDevices( initialData = basicUseCase.initializeConfigurationAndKeys(), accessToken = accessToken @@ -518,15 +542,15 @@ class DefaultIdpUseCase( ) = runCatching { block(false) - }.recoverCatching { e -> - val isRetryable = (e as? ApiCallException)?.let { - it.response.code() == HttpURLConnection.HTTP_FORBIDDEN || - it.response.code() == HttpURLConnection.HTTP_UNAUTHORIZED + }.recoverCatching { exception -> + Napier.e { "retrying due to ${exception.stackTraceToString()}" } + val isRetryable = (exception as? ApiCallException)?.let { + it.response.code() == HTTP_FORBIDDEN || it.response.code() == HTTP_UNAUTHORIZED } ?: false if (isRetryable) { block(true) } else { - throw e + throw exception } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ExternalAuthenticationPreferences.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ExternalAuthenticationPreferences.kt index e0190a34..c5c8998b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ExternalAuthenticationPreferences.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/ExternalAuthenticationPreferences.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetHealthInsuranceAppIdpsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetHealthInsuranceAppIdpsUseCase.kt index 5f30cc3b..37e43a8e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetHealthInsuranceAppIdpsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetHealthInsuranceAppIdpsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase @@ -43,7 +43,7 @@ class GetHealthInsuranceAppIdpsUseCase( } @Requirement( - "A_22296-01#1", + "A_23082#1", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Load list of external authenticators for Gesundheit ID." ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetUniversalLinkForHealthInsuranceAppsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetUniversalLinkForHealthInsuranceAppsUseCase.kt index 954ee1bd..9ae54a2a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetUniversalLinkForHealthInsuranceAppsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/GetUniversalLinkForHealthInsuranceAppsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt index 9d1b3be6..5086aa4d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpAlternateAuthenticationUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpAlternateAuthenticationUseCase.kt index 0b6d78b3..561c7563 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpAlternateAuthenticationUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpAlternateAuthenticationUseCase.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.idp.usecase import de.gematik.ti.erp.app.Requirement @@ -64,12 +66,6 @@ class IdpAlternateAuthenticationUseCase( encodeDefaults = true } - @Requirement( - "A_21591", - "A_21600", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Gather device info and register with health card." - ) suspend fun registerDeviceWithHealthCard( initialData: IdpInitialData, accessToken: String, @@ -145,11 +141,7 @@ class IdpAlternateAuthenticationUseCase( } // tag::DeletePairedDevicesUseCase[] - @Requirement( - "A_21443", - sourceSpecification = "gemF_Biometrie", - rationale = "Delete pairing for device." - ) + suspend fun deletePairedDevice( initialData: IdpInitialData, accessToken: String, @@ -163,6 +155,13 @@ class IdpAlternateAuthenticationUseCase( idpPukEncKey = pukEncKey.jws.publicKey ) + // TODO: This needs to be handled properly + @Requirement( + "O.Source_5#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Failure while deleting pairing", + codeLines = 6 + ) repository.deletePairing( url = config.pairingEndpoint, token = encryptedAccessToken.compactSerialization, @@ -198,7 +197,6 @@ class IdpAlternateAuthenticationUseCase( challenge: IdpUnsignedChallenge, healthCardCertificate: ByteArray, authenticationMethod: AuthenticationMethod = AuthenticationMethod.Strong, - aliasOfSecureElementEntry: ByteArray, privateKeyOfSecureElementEntry: PrivateKey, signatureObjectOfSecureElementEntry: Signature @@ -218,9 +216,13 @@ class IdpAlternateAuthenticationUseCase( @Requirement( "O.Cryp_1#3", - "O.Cryp_4#3", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature via ecdh ephemeral-static [one time usage]" + ) + @Requirement( + "O.Cryp_4#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "One time usage for JWE ECDH-ES Encryption" ) val signedAuthData = buildSignedAuthenticationData(authData, privateKeyOfSecureElementEntry, signatureObjectOfSecureElementEntry) @@ -304,7 +306,7 @@ class IdpAlternateAuthenticationUseCase( subjectPublicKeyInfoOfSecureElement: SubjectPublicKeyInfo, healthCardCertificate: X509CertificateHolder ): PairingData { - require(keyAliasOfSecureElement.size == 32) + require(keyAliasOfSecureElement.size == 32) // TODO: throws IllegalArgumentException, probably we can make a custom exception for this return PairingData( subjectPublicKeyInfoOfSecureElement = Base64Url.encode( @@ -348,11 +350,11 @@ class IdpAlternateAuthenticationUseCase( deviceInformation = deviceInformation ) - @Requirement( + /*( "A_21416", sourceSpecification = "gemF_Biometrie", rationale = "Generate registration data and encrypt it with PuK_IDP_ENC." - ) + ) */ fun buildEncryptedRegistrationData( registrationData: RegistrationData, idpPukEncKey: PublicKey @@ -393,9 +395,13 @@ class IdpAlternateAuthenticationUseCase( @Requirement( "O.Cryp_1#4", - "O.Cryp_4#4", sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" + rationale = "Signature via ecdh ephemeral-static [one time usage]" + ) + @Requirement( + "O.Cryp_4#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "One time usage for JWE ECDH-ES Encryption" ) fun buildSignedAuthenticationData( authenticationData: AuthenticationData, @@ -412,11 +418,6 @@ class IdpAlternateAuthenticationUseCase( signature ) - @Requirement( - "A_21431", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Create and encrypt authentication data." - ) fun buildEncryptedSignedAuthenticationData( signedAuthenticationData: String, challengeExpiry: Long, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt index 07edad34..daf653c3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.idp.usecase import de.gematik.ti.erp.app.BCProvider @@ -118,7 +120,12 @@ class IdpBasicUseCase( ) { // //////////////////////////////////////////////////////////////////////////////////////// - + @Requirement( + "A_17207#3", + "GS-A_4357-02#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "Initialize required algorithms implemented using ECDSA." + ) suspend fun initializeConfigurationAndKeys(): IdpInitialData { cryptoInitializedLock.withLock { if (!isCryptoInitialized) { @@ -139,6 +146,12 @@ class IdpBasicUseCase( // retry try { repository.loadUncheckedIdpConfiguration().also { + @Requirement( + "O.Auth_10#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The application also checks for the expiration time-stamp of the IDP configuration.", + codeLines = 3 + ) checkIdpConfigurationValidity(it, Clock.System.now()) } } catch (e: Exception) { @@ -162,6 +175,11 @@ class IdpBasicUseCase( val state = IdpState.create() val nonce = IdpNonce.create() + @Requirement( + "A_20309#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "generation and hashing for codeChallenge" + ) val codeVerifier = generateCodeVerifier() val codeChallenge = generateCodeChallenge(codeVerifier) @@ -253,12 +271,7 @@ class IdpBasicUseCase( ) @Requirement( - "A_20527#1", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Returns the AUTHORIZATION_CODE (accessToken) and the SSO token." - ) - @Requirement( - "A_21327#1", + "A_21327#4", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Usage of idToken Payload / idToken is not persisted / " + "since we have automatic memory management, we can't delete the token. " + @@ -309,6 +322,11 @@ class IdpBasicUseCase( val codeFromRedirect = IdpService.extractQueryParameter(redirect, "code") + @Requirement( + "A_20283-01#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "post redirect and decrypt access token." + ) val idpTokenResult = postCodeAndDecryptAccessToken( config.tokenEndpoint, nonce = nonce, @@ -351,12 +369,7 @@ class IdpBasicUseCase( return redirect } - @Requirement( - "A_19908-01", - "GS-A_4357-01", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Fetch and check challenge." - ) + // GS-A_4357-01 suspend fun fetchAndCheckUnsignedChallenge( url: String, codeChallenge: String, @@ -395,13 +408,6 @@ class IdpBasicUseCase( ) } - @Requirement( - "A_20529-01", - "A_20740#2", - "A_21414", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Post relevant information to IDP and decrypt access-token." - ) suspend fun postCodeAndDecryptAccessToken( url: String, nonce: IdpNonce, @@ -412,11 +418,15 @@ class IdpBasicUseCase( redirectUri: String ): IdpTokenResult { @Requirement( - "O.Cryp_3#2", - "O.Cryp_4#2", + "O.Cryp_3#3", sourceSpecification = "BSI-eRp-ePA", rationale = "AES Key-Generation and one time usage" ) + @Requirement( + "O.Cryp_4#5", + sourceSpecification = "BSI-eRp-ePA", + rationale = "One time usage for JWE ECDH-ES Encryption" + ) @Requirement( "GS-A_4389#1", sourceSpecification = "gemSpec_Krypt", @@ -435,6 +445,12 @@ class IdpBasicUseCase( code = code, redirectUri = redirectUri ).map { + @Requirement( + "A_20283-01#3", + "A_19938-01#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "decrypt access token and token validation." + ) val decryptedIdToken = decryptIdToken(it, symmetricalKey) val idTokenPayload = decryptedIdToken.apply { key = pukSigKey.jws.publicKey @@ -483,8 +499,8 @@ class IdpBasicUseCase( } @Requirement( - "A_20309", - sourceSpecification = "gemSpec_eRp_FdV", + "A_20309#2", + sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Generate code verifier using SecureRandome." ) fun generateCodeVerifier(): String { @@ -497,6 +513,11 @@ class IdpBasicUseCase( ) } + @Requirement( + "A_20309#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Hashing of code verifier to generate code challenge." + ) fun generateCodeChallenge(codeVerifier: String): String { // https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 return Base64Url.encode( @@ -506,20 +527,48 @@ class IdpBasicUseCase( ) } + // A_20617-01, @Requirement( - "A_20512", - "A_20614#2", - "A_20617-01#3", - "A_21218#2", - "A_22296-01#2", + "A_23082#3", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Get and validate discovery document." ) + @Requirement( + "O.Ntwk_4#1", + "O.Resi_6#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Get and validate discovery document." + ) + @Requirement( + "A_21218#5", + sourceSpecification = "gemSpec_Krypt", + rationale = "Check OCSP validity." + ) + @Requirement( + "A_20512#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Get and validate discovery document.", + codeLines = 3 + ) suspend fun checkIdpConfigurationValidity(config: IdpData.IdpConfiguration, timestamp: Instant) { truststoreUseCase.checkIdpCertificate(config.certificate, true) val claims = JwtClaims().apply { issuedAt = NumericDate.fromMilliseconds(config.issueTimestamp.toEpochMilliseconds()) + @Requirement( + "O.Auth_10#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The application also checks for the expiration time-stamp of the IDP configuration.", + codeLines = 3 + ) + @Requirement( + "A_20512#2,", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "The application also checks for the expiration time-stamp of the IDP configuration. " + + "The discoveryDocumentMaxValiditySeconds and discoveryDocumentMaxValidityMinutes value is used to set " + + "the maximum validity time of 24 hours.", + codeLines = 3 + ) expirationTime = NumericDate.fromMilliseconds(config.expirationTimestamp.toEpochMilliseconds()) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt index 467da011..19a8e9eb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt index c61adc8a..3427f805 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt index 64078dc6..440dba3c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt index 952e314e..1981e878 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt @@ -1,20 +1,21 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + @file:Suppress("LongParameterList") package de.gematik.ti.erp.app.idp.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt index ad4dd0b1..86165425 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase @@ -48,4 +48,14 @@ class RefreshFlowException : IOException { this.isUserAction = userActionRequired this.ssoToken = ssoToken } + + companion object { + fun Throwable.isUserActionRequired(): Boolean { + return this is RefreshFlowException && this.isUserAction + } + + fun Throwable.isUserActionNotRequired(): Boolean { + return !(this is RefreshFlowException && this.isUserAction) + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RemoveAuthenticationUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RemoveAuthenticationUseCase.kt index 9c97e949..22b59e05 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RemoveAuthenticationUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RemoveAuthenticationUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase @@ -25,22 +25,14 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +// A_21603 Invalidate/delete session data upon logout. +// since we have automatic memory management, we can't delete the token. +// Due to the use of frameworks we have sensitive data as immutable objects and hence cannot override it @Requirement( - "A_20186", - "A_21326", - "A_21327", - "A_20499-01", - "A_21603", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Invalidate/delete session data upon logout. " + - "since we have automatic memory management, we can't delete the token. " + - "Due to the use of frameworks we have sensitive data as immutable objects and hence " + - "cannot override it" -) -@Requirement( - "O.Tokn_6#4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate config and token " + "A_21326#3", + "A_21327#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Auth-token invalidated, removed on logout." ) class RemoveAuthenticationUseCase( private val repository: IdpRepository, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/mapper/InvoiceErrorMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/mapper/InvoiceErrorMapper.kt new file mode 100644 index 00000000..69985261 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/mapper/InvoiceErrorMapper.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.mapper + +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.api.httpErrorState +import de.gematik.ti.erp.app.invoice.model.InvoiceResult +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceError +import io.github.aakira.napier.Napier +import kotlinx.serialization.json.JsonElement + +inline fun Result.mapUnitToInvoiceError( + transform: (Unit) -> InvoiceResult +): Result { + return try { + when { + isSuccess -> { + runCatching { transform(this.getOrThrow()) } + } + + else -> { + when (val exception = this.exceptionOrNull()) { + is ApiCallException -> Result.failure(InvoiceError(exception.response.httpErrorState())) + else -> Result.failure(InvoiceError(HttpErrorState.Unknown)) + } + } + } + } catch (e: Exception) { + Napier.e { "ErrorOnDeletion: ${e.message}" } + Result.failure(InvoiceError(HttpErrorState.ErrorWithCause(e.message ?: "Unknown error on invoice charge bundle download"))) + } +} + +inline fun Result.mapJsonToInvoiceError( + transform: (value: JsonElement) -> InvoiceResult +): Result { + return try { + when { + isSuccess -> { + runCatching { transform(this.getOrThrow()) } + } + + else -> { + when (val exception = this.exceptionOrNull()) { + is ApiCallException -> Result.failure(InvoiceError(exception.response.httpErrorState())) + else -> Result.failure(InvoiceError(HttpErrorState.Unknown)) + } + } + } + } catch (e: Exception) { + Napier.e { "ErrorOnChargeItemDownload: result is success but data not available ${e.message}" } + Result.failure(InvoiceError(HttpErrorState.ErrorWithCause(e.message ?: "Unknown error on invoice charge item download"))) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/HtmlTemplate.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/HtmlTemplate.kt index 8cad8568..8ff0fcd7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/HtmlTemplate.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/HtmlTemplate.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.model -import de.gematik.ti.erp.app.utils.toFormattedDate import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.toFormattedDate object PkvHtmlTemplate { private fun createOrganization( @@ -73,6 +73,7 @@ object PkvHtmlTemplate {
""".trimIndent() + @Suppress("CyclomaticComplexMethod") fun createPriceData( medicationRequest: SyncedTaskData.MedicationRequest, taskId: String, @@ -107,7 +108,7 @@ object PkvHtmlTemplate { } val text = when (medicationRequest.medication) { - is SyncedTaskData.MedicationPZN -> if (medicationRequest.medication.uniqueIdentifier == pzn) { + is SyncedTaskData.Medication -> if (medicationRequest.medication.identifier.pzn == pzn) { "wie verordnet" } else { it.text @@ -185,26 +186,13 @@ object PkvHtmlTemplate { return info } - fun joinMedicationInfo(medicationRequest: SyncedTaskData.MedicationRequest?): String { - return when (val medication = medicationRequest?.medication) { - is SyncedTaskData.MedicationPZN -> - "${medicationRequest.quantity}x ${medication.text} / " + - "${medication.amount?.numerator?.value} " + - "${medication.amount?.numerator?.unit} " + - "${medication.normSizeCode} " + "PZN: " + medication.uniqueIdentifier - is SyncedTaskData.MedicationCompounding -> - "${medicationRequest.quantity}x ${medication.text} / " + - "${medication.amount?.numerator?.value} " + - "${medication.amount?.numerator?.unit} " + "${medication.form} " - is SyncedTaskData.MedicationIngredient -> - "${medicationRequest.quantity}x ${medication.text} / " + - "${medication.amount?.numerator?.value} " + - "${medication.amount?.numerator?.unit} " + "${medication.form} " + - "${medication.normSizeCode} " - is SyncedTaskData.MedicationFreeText -> "${medicationRequest.quantity}x ${medication.text}" - else -> "" - } - } + fun joinMedicationInfo(medicationRequest: SyncedTaskData.MedicationRequest?): String = + medicationRequest?.medication?.let { medication -> + "${medicationRequest.quantity}x ${medication.text} / " + + "${medication.amount?.numerator?.value} " + + "${medication.amount?.numerator?.unit} " + + "${medication.normSizeCode} " + "PZN: " + medication.identifier.pzn + } ?: "" private fun createFeesPriceData( totalBruttoAmount: Double, @@ -255,7 +243,7 @@ object PkvHtmlTemplate { """.trimIndent() - fun createHTML(invoice: InvoiceData.PKVInvoice): String { + fun createHTML(invoice: InvoiceData.PKVInvoiceRecord): String { val patient = createPatient( patientName = invoice.patient.name ?: "", patientAddress = invoice.patient.address?.joinToHtmlString() ?: "", diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceData.kt index c46a084e..e0f027a4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceData.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.model -import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.createDMPayload import kotlinx.datetime.Instant @@ -33,13 +33,13 @@ object InvoiceData { DeliveryServiceCosts("06461110"); companion object { - fun isAnyOf(pzn: String): Boolean = values().any { it.value == pzn } + fun isAnyOf(pzn: String): Boolean = entries.any { it.value == pzn } - fun valueOfPZN(pzn: String) = SpecialPZN.values().find { it.value == pzn } + fun valueOfPZN(pzn: String) = entries.find { it.value == pzn } } } - data class PKVInvoice( + data class PKVInvoiceRecord( val profileId: String, val taskId: String, val accessCode: String, @@ -51,7 +51,8 @@ object InvoiceData { val medicationRequest: SyncedTaskData.MedicationRequest, val whenHandedOver: FhirTemporal?, val invoice: Invoice, - val dmcPayload: String = createDMPayload(listOf("ChargeItem/$taskId?ac=$accessCode")) + val dmcPayload: String = createDMPayload(listOf("ChargeItem/$taskId?ac=$accessCode")), + val consumed: Boolean ) data class Invoice( @@ -86,4 +87,9 @@ object InvoiceData { } data class PriceComponent(val value: Double, val tax: Double) + + data class InvoiceStatus( + val taskId: String, + val consumed: Boolean + ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceResult.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceResult.kt new file mode 100644 index 00000000..86dcb2bb --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/model/InvoiceResult.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.model + +import de.gematik.ti.erp.app.api.HttpErrorState + +sealed interface InvoiceResult { + sealed interface InvoiceSuccess : InvoiceResult { + + data object SuccessOnChargeItemBundleDownload : InvoiceSuccess + + data class SuccessOnChargeItemDownload( + val total: Int, + val downloaded: Int + ) : InvoiceSuccess + + data class SuccessOnChargeItemDownloadWithErrors( + val total: Int, + val downloaded: Int, + val errors: List + ) : InvoiceSuccess + + data object SuccessOnDeletion : InvoiceSuccess + } + + data class UserNotLoggedInError(val id: String) : InvoiceResult, Throwable() + + data class InvoiceError(val errorState: HttpErrorState) : InvoiceResult, Throwable() + data class InvoiceCombinedError(val errorStates: List) : InvoiceResult, Throwable() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/DefaultInvoiceRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/DefaultInvoiceRepository.kt new file mode 100644 index 00000000..765926b9 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/DefaultInvoiceRepository.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.repository + +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.fhir.model.extractTaskIdsFromChargeItemBundle +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant +import kotlinx.serialization.json.JsonElement +import java.net.HttpURLConnection + +private const val InvoiceMaxPageSize = 25 + +class DefaultInvoiceRepository( + private val remoteDataSource: InvoiceRemoteDataSource, + private val localDataSource: InvoiceLocalDataSource, + private val dispatchers: DispatchProvider +) : InvoiceRepository, ResourcePaging(dispatchers, InvoiceMaxPageSize, maxPages = 1) { + + override fun getLatestTimeStamp(profileId: ProfileIdentifier): Flow = + localDataSource.latestInvoiceModifiedTimestamp(profileId).map { it.toTimestampString() } + + override suspend fun downloadChargeItemBundle( + profileId: ProfileIdentifier, + lastUpdated: String? + ): Result = + remoteDataSource.getChargeItems( + profileId = profileId, + lastUpdated = lastUpdated + ) + + override suspend fun downloadChargeItemByTaskId( + profileId: ProfileIdentifier, + taskId: String + ): Result = remoteDataSource.getChargeItemBundleById(profileId, taskId) + + override suspend fun downloadInvoices(profileId: ProfileIdentifier): Result = + downloadPaged(profileId) { prev: Int?, next: Int -> + (prev ?: 0) + next + }.map { + it ?: 0 + } + + override fun invoices(profileId: ProfileIdentifier): Flow> = + localDataSource.loadInvoices(profileId) + + override fun getInvoiceTaskIdAndConsumedStatus(profileId: ProfileIdentifier): Flow> = + localDataSource.getInvoiceTaskIdAndConsumedStatus(profileId) + + override suspend fun updateInvoiceCommunicationStatus(taskId: String, consumed: Boolean) { + localDataSource.updateInvoiceCommunicationStatus(taskId, consumed) + } + + override fun hasUnreadInvoiceMessages(taskIds: List): Flow = + localDataSource.hasUnreadInvoiceMessages(taskIds) + + override fun invoiceByTaskId(taskId: String): Flow = + localDataSource.loadInvoiceByTaskId(taskId) + + override suspend fun saveInvoice(profileId: ProfileIdentifier, bundle: JsonElement) { + localDataSource.saveInvoice(profileId, bundle) + } + + override suspend fun deleteRemoteInvoiceById( + taskId: String, + profileId: ProfileIdentifier + ): Result = withContext(dispatchers.io) { + val result = remoteDataSource.deleteChargeItemById(profileId, taskId) + .onSuccess { + deleteLocalInvoice(taskId) + }.onFailure { + if (it is ApiCallException) { + when (it.response.code()) { + HttpURLConnection.HTTP_NOT_FOUND, + HttpURLConnection.HTTP_GONE -> + localDataSource.deleteInvoiceById(taskId) + } + } + } + result + } + + override fun loadInvoiceAttachments(taskId: String): List>? = + localDataSource.loadInvoiceAttachments(taskId) + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + localDataSource.latestInvoiceModifiedTimestamp(profileId).first() + + override suspend fun deleteLocalInvoice(taskId: String) = + localDataSource.deleteInvoiceById(taskId) + + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> = + remoteDataSource.getChargeItems( + profileId = profileId, + lastUpdated = timestamp, + count = count + ).mapCatching { fhirBundle -> + withContext(dispatchers.io) { + val (total, taskIds) = extractTaskIdsFromChargeItemBundle(fhirBundle) + + supervisorScope { + val results = taskIds.map { taskId -> + async { + downloadInvoiceWithBundle(taskId = taskId, profileId = profileId) + } + }.awaitAll() + + // return number of bundles saved to db + ResourceResult(total, results.size) + } + } + } + + private suspend fun downloadInvoiceWithBundle( + taskId: String, + profileId: ProfileIdentifier + ) = withContext(dispatchers.io) { + remoteDataSource.getChargeItemBundleById(profileId, taskId).mapCatching { bundle -> + requireNotNull(localDataSource.saveInvoice(profileId, bundle)) + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceLocalDataSource.kt index 747bf31f..70fce657 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceLocalDataSource.kt @@ -1,36 +1,36 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.repository import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.ChargeableItemV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.DescriptionTypeV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.InvoiceEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.invoice.PriceComponentV1 import de.gematik.ti.erp.app.db.entities.v1.task.AccidentTypeV1 +import de.gematik.ti.erp.app.db.entities.v1.task.CoverageTypeV1 import de.gematik.ti.erp.app.db.entities.v1.task.IngredientEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.InsuranceInformationEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationCategoryV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MedicationProfileV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationRequestEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MultiplePrescriptionInfoEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.OrganizationEntityV1 @@ -44,21 +44,25 @@ import de.gematik.ti.erp.app.db.toRealmInstant import de.gematik.ti.erp.app.db.tryWrite import de.gematik.ti.erp.app.fhir.model.AccidentType import de.gematik.ti.erp.app.fhir.model.MedicationCategory -import de.gematik.ti.erp.app.fhir.model.MedicationProfile import de.gematik.ti.erp.app.fhir.model.extractBinary -import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.fhir.model.extractInvoiceBundle import de.gematik.ti.erp.app.fhir.model.extractInvoiceKBVAndErpPrBundle import de.gematik.ti.erp.app.fhir.model.extractKBVBundle +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.repository.toMedication import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.github.aakira.napier.Napier import io.realm.kotlin.Realm import io.realm.kotlin.ext.query import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.query.Sort import io.realm.kotlin.query.max import io.realm.kotlin.types.RealmInstant import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -66,7 +70,7 @@ import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.serialization.json.JsonElement - +@Suppress("SpreadOperator") class InvoiceLocalDataSource( private val realm: Realm ) { @@ -81,7 +85,7 @@ class InvoiceLocalDataSource( private val mutex = Mutex() - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod") suspend fun saveInvoice(profileId: ProfileIdentifier, bundle: JsonElement) = mutex.withLock { realm.tryWrite { queryFirst("id = $0", profileId)?.let { profile -> @@ -188,10 +192,11 @@ class InvoiceLocalDataSource( this.practitionerIdentifier = practitionerIdentifier } }, - processInsuranceInformation = { name, statusCode -> + processInsuranceInformation = { name, statusCode, coverageType -> InsuranceInformationEntityV1().apply { this.name = name this.statusCode = statusCode + this.coverageType = CoverageTypeV1.mapTo(coverageType) } }, processAddress = { line, postalCode, city -> @@ -214,17 +219,18 @@ class InvoiceLocalDataSource( this.denominator = denominator } }, - processIngredient = { text, form, number, amount, strength -> + processIngredient = { text, form, identifier, amount, strength -> IngredientEntityV1().apply { this.text = text this.form = form this.number = number this.amount = amount + this.identifier = identifier.toIdentifierEntityV1() this.strength = strength } }, - processMedication = { text, - medicationProfile, + processMedication = + { text, medicationCategory, form, amount, @@ -232,19 +238,13 @@ class InvoiceLocalDataSource( manufacturingInstructions, packaging, normSizeCode, - uniqueIdentifier, + identifier, + ingredientMedications, ingredients, _, _ -> MedicationEntityV1().apply { this.text = text ?: "" - this.medicationProfile = when (medicationProfile) { - MedicationProfile.PZN -> MedicationProfileV1.PZN - MedicationProfile.COMPOUNDING -> MedicationProfileV1.COMPOUNDING - MedicationProfile.INGREDIENT -> MedicationProfileV1.INGREDIENT - MedicationProfile.FREETEXT -> MedicationProfileV1.FREETEXT - else -> MedicationProfileV1.UNKNOWN - } this.medicationCategory = when (medicationCategory) { MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL @@ -260,7 +260,8 @@ class InvoiceLocalDataSource( this.manufacturingInstructions = manufacturingInstructions this.packaging = packaging this.normSizeCode = normSizeCode - this.uniqueIdentifier = uniqueIdentifier + this.ingredientMedications = ingredientMedications.toRealmList() + this.identifier = identifier.toIdentifierEntityV1() this.ingredients = ingredients.toRealmList() } }, @@ -272,8 +273,8 @@ class InvoiceLocalDataSource( this.end = end?.toInstant(TimeZone.UTC)?.toRealmInstant() } }, - processMedicationRequest = { - authoredOn, + processMedicationRequest = + { authoredOn, dateOfAccident, location, accidentType, @@ -365,8 +366,9 @@ class InvoiceLocalDataSource( } } - private fun PKVInvoiceEntityV1.toPKVInvoice(): InvoiceData.PKVInvoice = - InvoiceData.PKVInvoice( + @Suppress("CyclomaticComplexMethod") + private fun PKVInvoiceEntityV1.toPKVInvoice(): InvoiceData.PKVInvoiceRecord = + InvoiceData.PKVInvoiceRecord( profileId = this.parent?.id ?: "", timestamp = this.timestamp.toInstant(), pharmacyOrganization = SyncedTaskData.Organization( @@ -422,12 +424,12 @@ class InvoiceLocalDataSource( dosageInstruction = this.medicationRequest?.dosageInstruction, multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo( indicator = this.medicationRequest?.multiplePrescriptionInfo?.indicator ?: false, - numbering = SyncedTaskData.Ratio( - numerator = SyncedTaskData.Quantity( + numbering = Ratio( + numerator = Quantity( value = this.medicationRequest?.multiplePrescriptionInfo?.numbering?.numerator?.value ?: "", unit = "" ), - denominator = SyncedTaskData.Quantity( + denominator = Quantity( value = this.medicationRequest?.multiplePrescriptionInfo?.numbering?.denominator?.value ?: "", unit = "" @@ -460,18 +462,47 @@ class InvoiceLocalDataSource( it.toChargeableItem() } ?: listOf(), additionalInformation = this.invoice?.additionalInformation?.toList() ?: listOf() - ) + ), + consumed = this.consumed ) - fun loadInvoices(profileId: ProfileIdentifier): Flow> = + fun loadInvoices(profileId: ProfileIdentifier): Flow> = realm.query("parent.id = $0", profileId) .asFlow() .map { invoices -> - invoices.list.map { invoice -> + val result = invoices.list.map { invoice -> invoice.toPKVInvoice() } + result } + fun loadInvoiceByTaskId(taskId: String): Flow = + realm.query("taskId = $0", taskId) + .sort("timestamp", Sort.DESCENDING) + .asFlow() + .map { result -> + result.list.firstOrNull()?.toPKVInvoice() + } + + fun getInvoiceTaskIdAndConsumedStatus(profileId: ProfileIdentifier): Flow> = + realm.query("parent.id = $0", profileId) + .asFlow() + .map { invoices -> + invoices.list.map { invoice -> + InvoiceData.InvoiceStatus(taskId = invoice.taskId, consumed = invoice.consumed) + } + } + + suspend fun updateInvoiceCommunicationStatus(taskId: String, consumed: Boolean) { + realm.tryWrite { + this.query("taskId == $0", taskId) + .first() + .find()?.apply { + this.consumed = consumed + } ?: Napier.w("Task ID $taskId not found, unable to update consumed status") + } + } + fun loadInvoiceAttachments(taskId: String) = realm.queryFirst("taskId = $0", taskId)?.let { listOf( @@ -481,19 +512,10 @@ class InvoiceLocalDataSource( ) } - fun loadInvoiceById(taskId: String): Flow = - realm.query("taskId = $0", taskId) - .first() - .asFlow() - .map { invoice -> - invoice.obj?.toPKVInvoice() - } - - suspend fun deleteInvoiceById(taskId: String) { - realm.tryWrite { + suspend fun deleteInvoiceById(taskId: String): Unit = + realm.tryWrite { queryFirst("taskId = $0", taskId)?.let { delete(it) } } - } private fun ChargeableItemV1.toChargeableItem() = InvoiceData.ChargeableItem( description = when (this.descriptionTypeV1) { @@ -508,4 +530,21 @@ class InvoiceLocalDataSource( tax = this.price?.tax ?: 0.0 ) ) + + fun hasUnreadInvoiceMessages(taskIds: List): Flow { + return if (taskIds.isEmpty()) { + flowOf(false) + } else { + val orQuery = taskIds.indices.joinToString(" || ") { "taskId = $$it" } + + realm.query( + orQuery, + *taskIds.toTypedArray() + ) + .query("consumed = false") + .count() + .asFlow() + .map { it > 0 } + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRemoteDataSource.kt index 4eaea1b9..0ae4d348 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt index 261f13d4..2ff66fdb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt @@ -1,121 +1,56 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.repository -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.api.ResourcePaging -import de.gematik.ti.erp.app.fhir.model.extractTaskIdsFromChargeItemBundle +import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant import kotlinx.serialization.json.JsonElement -import java.net.HttpURLConnection - -private const val InvoiceMaxPageSize = 25 - -class InvoiceRepository( - private val remoteDataSource: InvoiceRemoteDataSource, - private val localDataSource: InvoiceLocalDataSource, - private val dispatchers: DispatchProvider -) : ResourcePaging(dispatchers, InvoiceMaxPageSize, maxPages = 1) { - - suspend fun downloadInvoices(profileId: ProfileIdentifier) = downloadPaged(profileId) { prev: Int?, next: Int -> - (prev ?: 0) + next - }.map { - it ?: 0 - } - -// todo: Dipachers should not to be used here! - fun invoices(profileId: ProfileIdentifier) = - localDataSource.loadInvoices(profileId).flowOn(dispatchers.io) - - fun invoiceById(taskId: String) = - localDataSource.loadInvoiceById(taskId).flowOn(dispatchers.io) - - suspend fun saveInvoice(profileId: ProfileIdentifier, bundle: JsonElement) { - localDataSource.saveInvoice(profileId, bundle) - } - - override val tag: String = "InvoiceRepository" - - override suspend fun downloadResource( - profileId: ProfileIdentifier, - timestamp: String?, - count: Int? - ): Result> = - remoteDataSource.getChargeItems( - profileId = profileId, - lastUpdated = timestamp, - count = count - ).mapCatching { fhirBundle -> - withContext(dispatchers.io) { - val (total, taskIds) = extractTaskIdsFromChargeItemBundle(fhirBundle) - - supervisorScope { - val results = taskIds.map { taskId -> - async { - downloadInvoiceWithBundle(taskId = taskId, profileId = profileId) - } - }.awaitAll() - - // return number of bundles saved to db - ResourceResult(total, results.size) - } - } - } - private suspend fun downloadInvoiceWithBundle( - taskId: String, - profileId: ProfileIdentifier - ) = withContext(dispatchers.io) { - remoteDataSource.getChargeItemBundleById(profileId, taskId).mapCatching { bundle -> - requireNotNull(localDataSource.saveInvoice(profileId, bundle)) - } - } - - suspend fun deleteInvoiceById( - taskId: String, - profileId: ProfileIdentifier - ) = withContext(dispatchers.io) { - val result = remoteDataSource.deleteChargeItemById(profileId, taskId) - .onSuccess { - localDataSource.deleteInvoiceById(taskId) - }.onFailure { - if (it is ApiCallException) { - when (it.response.code()) { - HttpURLConnection.HTTP_NOT_FOUND, - HttpURLConnection.HTTP_GONE -> - localDataSource.deleteInvoiceById(taskId) - } - } - } - result - } - - fun loadInvoiceAttachments(taskId: String) = - localDataSource.loadInvoiceAttachments(taskId) - - override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = - localDataSource.latestInvoiceModifiedTimestamp(profileId).first() + +interface InvoiceRepository { + + fun getLatestTimeStamp(profileId: ProfileIdentifier): Flow + + suspend fun downloadChargeItemBundle(profileId: ProfileIdentifier, lastUpdated: String?): Result + + suspend fun downloadChargeItemByTaskId(profileId: ProfileIdentifier, taskId: String): Result + + suspend fun downloadInvoices(profileId: ProfileIdentifier): Result + + fun invoices(profileId: ProfileIdentifier): Flow> + + fun getInvoiceTaskIdAndConsumedStatus(profileId: ProfileIdentifier): Flow> + + suspend fun updateInvoiceCommunicationStatus(taskId: String, consumed: Boolean) + + fun hasUnreadInvoiceMessages(taskIds: List): Flow + + fun invoiceByTaskId(taskId: String): Flow + + suspend fun saveInvoice(profileId: ProfileIdentifier, bundle: JsonElement) + + suspend fun deleteRemoteInvoiceById(taskId: String, profileId: ProfileIdentifier): Result + + fun loadInvoiceAttachments(taskId: String): List>? + + suspend fun deleteLocalInvoice(taskId: String) + + suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoices.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoices.kt new file mode 100644 index 00000000..e9fd27c7 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoices.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class DeleteAllLocalInvoices( + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + taskIds: List + ): Result = withContext(dispatcher) { + try { + taskIds.forEach { + invoiceRepository.deleteLocalInvoice(taskId = it) + } + return@withContext Result.success(Unit) + } catch (e: Throwable) { + return@withContext Result.failure(e) + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteInvoiceUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteInvoiceUseCase.kt new file mode 100644 index 00000000..d3dff1c5 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteInvoiceUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.invoice.mapper.mapUnitToInvoiceError +import de.gematik.ti.erp.app.invoice.model.InvoiceResult +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +class DeleteInvoiceUseCase( + private val profileRepository: ProfileRepository, + private val invoiceRepository: InvoiceRepository, + private val dispatchers: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + taskId: String, + profileId: ProfileIdentifier + ): Result = + withContext(dispatchers) { + val isSsoTokenValid = profileRepository.isSsoTokenValid(profileId).first() + if (!isSsoTokenValid) { + return@withContext Result.failure(InvoiceResult.UserNotLoggedInError(profileId)) + } + invoiceRepository.deleteRemoteInvoiceById(taskId = taskId, profileId = profileId) + .mapUnitToInvoiceError { + invoiceRepository.deleteLocalInvoice(taskId = taskId) + InvoiceResult.InvoiceSuccess.SuccessOnDeletion + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DownloadInvoicesUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DownloadInvoicesUseCase.kt new file mode 100644 index 00000000..7df0c2e2 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/DownloadInvoicesUseCase.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.api.HttpErrorState.Unknown +import de.gematik.ti.erp.app.fhir.model.extractTaskIdsFromChargeItemBundle +import de.gematik.ti.erp.app.invoice.mapper.mapJsonToInvoiceError +import de.gematik.ti.erp.app.invoice.model.InvoiceResult +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceError +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceSuccess +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceSuccess.SuccessOnChargeItemBundleDownload +import de.gematik.ti.erp.app.invoice.model.InvoiceResult.InvoiceSuccess.SuccessOnChargeItemDownload +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext + +class DownloadInvoicesUseCase( + private val profileRepository: ProfileRepository, + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + profileId: String, + onDownloadStarted: () -> Unit + ): Flow> = + withContext(dispatcher) { + val isSsoTokenValid = profileRepository.isSsoTokenValid(profileId).first() + if (!isSsoTokenValid) { + return@withContext flowOf(Result.failure(InvoiceResult.UserNotLoggedInError(profileId))) + } + onDownloadStarted() + Napier.i( + tag = DownloadInvoicesUseCase::class.simpleName, + message = "starting invoices download" + ) + invoiceRepository.getLatestTimeStamp(profileId) + .map { latestTimeStamp -> + invoiceRepository.downloadChargeItemBundle( + profileId = profileId, + lastUpdated = latestTimeStamp + ).mapJsonToInvoiceError { fhirJsonBundle -> + val (total, taskIds) = extractTaskIdsFromChargeItemBundle(fhirJsonBundle) // TODO: Needs a mapper class or extension method + supervisorScope { + val invoiceResults = taskIds.map { taskId -> + invoiceRepository.downloadChargeItemByTaskId(profileId, taskId) + .mapJsonToInvoiceError { fhirJsonTask -> + invoiceRepository.saveInvoice(profileId, fhirJsonTask) // TODO: Mapper needs to be extracted before save + SuccessOnChargeItemBundleDownload + } + } + val totalDownloads = invoiceResults.filter { it.isSuccess }.size + val totalDownloadsFailures = invoiceResults.filter { it.isFailure }.size + Napier.i( + tag = DownloadInvoicesUseCase::class.simpleName, + message = "total downloads $totalDownloads" + ) + when { + taskIds.size == totalDownloads && (totalDownloads > 0 || totalDownloadsFailures <= 0) -> SuccessOnChargeItemDownload( + total = total, + downloaded = totalDownloads + ) + // TODO: Needs to be used in the UI to inform the user that they need to download again + invoiceResults.any { it.isSuccess } -> InvoiceSuccess.SuccessOnChargeItemDownloadWithErrors( + total = total, + downloaded = totalDownloads, + errors = invoiceResults.filter { it.isFailure }.map { it.exceptionOrNull() as? InvoiceError ?: InvoiceError(Unknown) } + ) + + else -> { + val httpErrors = if (invoiceResults.isNotEmpty()) { + invoiceResults.map { (it.getOrNull() as? InvoiceError)?.errorState ?: Unknown } + } else { + listOf(Unknown) + } + InvoiceResult.InvoiceCombinedError(errorStates = httpErrors) + } + } + } + } + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoiceByTaskIdUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoiceByTaskIdUseCase.kt index 2c3072e1..a3d9aa0f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoiceByTaskIdUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoiceByTaskIdUseCase.kt @@ -1,30 +1,33 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.usecase import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow - +import kotlinx.coroutines.flow.flowOn class GetInvoiceByTaskIdUseCase( - private val invoiceRepository: InvoiceRepository + private val invoiceRepository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - operator fun invoke(taskId: String): Flow = - invoiceRepository.invoiceById(taskId) + operator fun invoke(taskId: String): Flow = + invoiceRepository.invoiceByTaskId(taskId).flowOn(dispatcher) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoicesByProfileUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoicesByProfileUseCase.kt new file mode 100644 index 00000000..57709e27 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/GetInvoicesByProfileUseCase.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +class GetInvoicesByProfileUseCase( + private val invoiceRepository: InvoiceRepository, + private val timeZone: TimeZone = TimeZone.currentSystemDefault(), + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(profileId: String): Flow>> = + invoiceRepository.invoices(profileId) + .map { invoices -> + invoices + .sortedByDescending { it.timestamp } + .groupBy { Year(it.timestamp.toLocalDateTime(timeZone).year) } + } + .flowOn(dispatcher) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt deleted file mode 100644 index 90e9fa63..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.invoice.usecase - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.invoice.model.InvoiceData -import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -class InvoiceUseCase( - private val invoiceRepository: InvoiceRepository, - private val dispatchers: DispatchProvider -) { - fun invoicesFlow(profileId: ProfileIdentifier): Flow> = - invoiceRepository.invoices(profileId).flowOn(dispatchers.io) - - fun invoices(profileId: ProfileIdentifier): Flow>> = - invoicesFlow(profileId).map { invoices -> - invoices.sortedWith( - compareByDescending { - it.timestamp - } - ).groupBy { - it.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()).year - } - } - - suspend fun deleteInvoice(profileId: ProfileIdentifier, taskId: String): Result { - return invoiceRepository.deleteInvoiceById(profileId = profileId, taskId = taskId) - } - - private class Request( - val resultChannel: Channel>, - val forProfileId: ProfileIdentifier - ) - - private val scope = CoroutineScope(dispatchers.io) - - private val requestChannel = - Channel(onUndeliveredElement = { it.resultChannel.close(CancellationException()) }) - - private val _refreshInProgress = MutableStateFlow(false) - val refreshInProgress: StateFlow - get() = _refreshInProgress - - init { - scope.launch { - for (request in requestChannel) { - _refreshInProgress.value = true - Napier.d { "Start refreshing as per request" } - - val profileId = request.forProfileId - - val result = runCatching { - val nrOfNewInvoices = invoiceRepository.downloadInvoices(profileId).getOrThrow() - nrOfNewInvoices - } - request.resultChannel.trySend(result) - - Napier.d { "Finished refreshing" } - _refreshInProgress.value = false - } - } - } - - private suspend fun download(profileId: ProfileIdentifier): Result { - val resultChannel = Channel>() - try { - requestChannel.send(Request(resultChannel = resultChannel, forProfileId = profileId)) - - return resultChannel.receive() - } catch (cancellation: CancellationException) { - Napier.d { "Cancelled waiting for result of refresh request" } - withContext(NonCancellable) { - resultChannel.close(cancellation) - } - throw cancellation - } - } - - fun downloadInvoices(profileId: ProfileIdentifier): Flow = - flow { - emit(download(profileId).getOrThrow()) - } - - fun loadAttachments(taskId: String) = - invoiceRepository.loadInvoiceAttachments(taskId) - - fun invoiceById(taskId: String): Flow = - invoiceRepository.invoiceById(taskId) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/SaveInvoiceUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/SaveInvoiceUseCase.kt new file mode 100644 index 00000000..baa50fc4 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/SaveInvoiceUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonElement + +class SaveInvoiceUseCase( + private val repository: InvoiceRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + profileId: ProfileIdentifier, + bundle: JsonElement + ) { + withContext(dispatcher) { + repository.saveInvoice(profileId, bundle) + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/DateEvent.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/DateEvent.kt new file mode 100644 index 00000000..d32fa461 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/DateEvent.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import androidx.compose.runtime.Stable +import kotlinx.datetime.LocalDate + +@Stable +sealed class DateEvent { + data class StartDate(val date: LocalDate) : DateEvent() + data class EndDate(val date: LocalDate) : DateEvent() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationDosage.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationDosage.kt new file mode 100644 index 00000000..c0a72648 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationDosage.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +data class MedicationDosage( + val form: String, + val ratio: String +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotification.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotification.kt new file mode 100644 index 00000000..9a4f02f6 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotification.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import kotlinx.datetime.LocalTime +import java.util.UUID + +data class MedicationNotification( + val id: String = UUID.randomUUID().toString(), + val time: LocalTime, + val dosage: MedicationDosage +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotificationMessage.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotificationMessage.kt new file mode 100644 index 00000000..61bbde55 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationNotificationMessage.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +data class MedicationNotificationMessage( + val title: String, + val body: String +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanDosageInstruction.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanDosageInstruction.kt new file mode 100644 index 00000000..c3db3d49 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanDosageInstruction.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +sealed interface MedicationPlanDosageInstruction { + enum class DayTime { + MORNING, + NOON, + EVENING, + NIGHT, + } + + data class FreeText( + val text: String + ) : MedicationPlanDosageInstruction + + data object Empty : MedicationPlanDosageInstruction + + // DJ: The Doctor has given a dosage instruction on the package or in medication plan or ...? + data object External : MedicationPlanDosageInstruction + + data class Structured( + val text: String, + val interpretation: Map + ) : MedicationPlanDosageInstruction +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapper.kt new file mode 100644 index 00000000..96dc7fe1 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapper.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationDosageEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationNotificationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationScheduleEntityV1 +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.repository.toRatio +import de.gematik.ti.erp.app.utils.toLocalDate +import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.types.RealmList +import kotlinx.datetime.Clock +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime +import java.util.UUID +import kotlin.time.Duration.Companion.days + +private val MORNING_HOUR = LocalTime.parse("08:00") +private val NOON_HOUR = LocalTime.parse("12:00") +private val EVENING_HOUR = LocalTime.parse("18:00") +private val NIGHT_HOUR = LocalTime.parse("20:00") + +fun MedicationDosageEntityV1.toMedicationDosage(): MedicationDosage = MedicationDosage( + form = this.form, + ratio = this.ratio +) + +fun RealmList.toNotifications(): List = + this.map { notification -> + MedicationNotification( + time = LocalDateTime.parse(notification.time).time, + dosage = notification.dosage?.toMedicationDosage() ?: MedicationDosage("", ""), + id = notification.id + ) + } + +fun MedicationScheduleEntityV1.update(newMedicationSchedule: MedicationSchedule) { + this.apply { + this.start = newMedicationSchedule.start.toString() + this.end = newMedicationSchedule.end.toString() + this.title = newMedicationSchedule.message.title + this.body = newMedicationSchedule.message.body + this.taskId = newMedicationSchedule.taskId + this.notifications = newMedicationSchedule.notifications.map { notification -> + MedicationNotificationEntityV1().apply { + time = notification.time.toString() + dosage = MedicationDosageEntityV1().apply { + this.form = notification.dosage.form + this.ratio = notification.dosage.ratio + } + } + }.toRealmList() + } +} + +fun MedicationScheduleEntityV1.toMedicationSchedule() = + MedicationSchedule( + start = LocalDate.parse(this.start), + end = LocalDate.parse(this.end), + isActive = this.isActive, + message = MedicationNotificationMessage( + title = this.title, + body = this.body + ), + taskId = this.taskId, + profileId = this.profileId, + amount = this.amount.toRatio() ?: Ratio(Quantity("", ""), Quantity("", "")), + notifications = this.notifications.toNotifications() + ) + +fun PrescriptionData.Prescription.toMedicationSchedule( + now: Instant = Clock.System.now() +): MedicationSchedule { + when (this) { + is PrescriptionData.Scanned -> { + return MedicationSchedule( + start = now.toLocalDateTime(TimeZone.currentSystemDefault()).date, + end = now.toLocalDateTime(TimeZone.currentSystemDefault()).date.plus(DatePeriod(days = 10)), + isActive = false, + message = MedicationNotificationMessage( + title = this.name, + body = "" + ), + taskId = this.taskId, + profileId = this.profileId, + amount = null, + notifications = emptyList() + ) + } + + is PrescriptionData.Synced -> { + val dosageInstruction = parseInstruction(this.medicationRequest.dosageInstruction) + val amount = getAmount(this.medicationRequest) + + return MedicationSchedule( + start = now.toLocalDate(), + end = getCalculatedEndDate( + start = now, + amount = amount, + dosageInstruction = dosageInstruction, + form = medicationRequest.medication?.form ?: "" + ), + isActive = false, + message = MedicationNotificationMessage( + title = this.medicationRequest.medication?.name() ?: "", + body = "" + ), + amount = amount, + taskId = this.taskId, + profileId = this.profileId, + notifications = mapDosageInstructionToNotifications( + dosageInstruction, + medicationRequest.medication?.form + ) + ) + } + } +} + +fun getCalculatedEndDate( + start: Instant = Clock.System.now(), + amount: Ratio, + dosageInstruction: MedicationPlanDosageInstruction, + form: String +): LocalDate { + return if (pieceableForm.contains(form)) { + when (dosageInstruction) { + // 1-0-1 + is MedicationPlanDosageInstruction.Structured -> { + val currentTime = start.toLocalDateTime(TimeZone.currentSystemDefault()).time + + val amountToConsumePerDay = dosageInstruction.interpretation.entries.map { + it.value.toFloatOrNull() ?: 1f + }.sum() + + val amountToConsumeToDay = dosageInstruction.interpretation.map { (dayTime, amount) -> + val hour = when (dayTime) { + MedicationPlanDosageInstruction.DayTime.MORNING -> MORNING_HOUR + MedicationPlanDosageInstruction.DayTime.NOON -> NOON_HOUR + MedicationPlanDosageInstruction.DayTime.EVENING -> EVENING_HOUR + MedicationPlanDosageInstruction.DayTime.NIGHT -> NIGHT_HOUR + } + if (currentTime <= hour) { + amount.toFloatOrNull() ?: 0f + } else { + 0f + } + }.sum() + + val amountInPackage = amount?.numerator?.value?.toInt() ?: 1 + + val daysLeft = (amountInPackage / amountToConsumePerDay).takeIf { amountToConsumePerDay != 0f } ?: 0 + val adjustedDaysLeft = daysLeft.toLong() - if (amountToConsumeToDay == amountToConsumePerDay) 1 else 0 + start.plus(adjustedDaysLeft.days).toLocalDate() + } else -> start.toLocalDate() + } + } else { + start.toLocalDate() + } +} + +fun getAmount(medicationRequest: SyncedTaskData.MedicationRequest): Ratio { + val nrOfPackages = medicationRequest.quantity + return medicationRequest.medication?.let { medication -> + multiplyMedicationAmount(medication.amount, nrOfPackages) + } ?: Ratio( + numerator = Quantity( + value = "1", + unit = "" + ), + denominator = Quantity( + value = "1", + unit = "" + ) + ) +} + +fun multiplyMedicationAmount(value: Ratio?, multiplier: Int): Ratio? { + return value?.numerator?.value?.replace(",", ".")?.toFloatOrNull()?.let { number -> + val result = number * multiplier + val formattedResult = if (result % 1 == 0f) result.toInt().toString() else result.toString() + value.copy( + numerator = Quantity( + value = formattedResult, + unit = value.numerator.unit + ) + ) + } +} + +fun mapDosageInstructionToNotifications( + dosageInstruction: MedicationPlanDosageInstruction, + form: String? +): List { + return when (dosageInstruction) { + is MedicationPlanDosageInstruction.Structured -> { + dosageInstruction.interpretation.map { (dayTime, dosage) -> + MedicationNotification( + time = when (dayTime) { + MedicationPlanDosageInstruction.DayTime.MORNING -> MORNING_HOUR + MedicationPlanDosageInstruction.DayTime.NOON -> NOON_HOUR + MedicationPlanDosageInstruction.DayTime.EVENING -> EVENING_HOUR + MedicationPlanDosageInstruction.DayTime.NIGHT -> NIGHT_HOUR + }, + dosage = MedicationDosage( + form = form ?: "", + ratio = dosage + ), + id = UUID.randomUUID().toString() + ) + } + } + else -> emptyList() + } +} + +fun parseInstruction(dosageInstruction: String?): MedicationPlanDosageInstruction { + return dosageInstruction?.let { instruction -> + + val trimmedInstruction = trimInstruction(instruction) + val interpretation = interpretDosage(trimmedInstruction) + + if (interpretation.isEmpty()) { + when { + trimmedInstruction.lowercase() == "dj" -> MedicationPlanDosageInstruction.External + trimmedInstruction.isNotEmpty() -> MedicationPlanDosageInstruction.FreeText(instruction) + else -> MedicationPlanDosageInstruction.Empty + } + } else { + val filtered = interpretation.filter { it.value != "0" } + if (filtered.isEmpty()) { + MedicationPlanDosageInstruction.Empty + } else { + MedicationPlanDosageInstruction.Structured( + text = instruction, + interpretation = filtered + ) + } + } + } ?: MedicationPlanDosageInstruction.Empty +} + +fun trimInstruction(instruction: String): String = instruction.trimStart(' ', '<', '>').trimEnd(' ', '<', '>') + +private fun interpretDosage(cleanedDosage: String): Map { + val parts = cleanedDosage.split("-").map { it.trim() } + return parts.mapIndexedNotNull { index, part -> + when { + parts.size <= MedicationPlanDosageInstruction.DayTime.entries.size && + part.matches(Regex("[0-9,.½/ ]+")) -> { + MedicationPlanDosageInstruction.DayTime.entries.getOrNull(index)?.let { time -> + time to part + } + } + else -> { + null + } + } + }.toMap() +} + +// see kbvCodeMapping for more information +val pieceableForm = listOf( + "AMP", + "BEU", + "BON", + "BTA", + "DKA", + "DRA", + "DRM", + "FDA", + "FER", + "FMR", + "FTA", + "GLO", + "HKM", + "HKP", + "HPI", + "HVW", + "KAP", + "KDA", + "KGU", + "KLI", + "KLT", + "KMP", + "KMR", + "KOD", + "KTA", + "LTA", + "LUP", + "LUT", + "MRP", + "MTA", + "PAS", + "PEL", + "PEN", + "PER", + "RED", + "REK", + "RET", + "RKA", + "RUT", + "SMT", + "SUT", + "TAB", + "TAE", + "TKA", + "TLE", + "TMR", + "TRT", + "TSD", + "TSE", + "TVW", + "UTA", + "VKA", + "VTA", + "WKA", + "WKM", + "XGM", + "ZKA" +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationSchedule.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationSchedule.kt new file mode 100644 index 00000000..fc5dec03 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationSchedule.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import androidx.compose.runtime.Immutable +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.LocalDate + +@Immutable +data class MedicationSchedule( + val profileId: ProfileIdentifier, + val taskId: String, + val start: LocalDate, + val end: LocalDate, + val amount: Ratio?, + val isActive: Boolean, + val message: MedicationNotificationMessage, + val notifications: List +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/ProfileWithSchedules.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/ProfileWithSchedules.kt new file mode 100644 index 00000000..e10a61b9 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/model/ProfileWithSchedules.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +data class ProfileWithSchedules( + val profile: ProfilesUseCaseData.Profile, + val medicationSchedules: List +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanLocalDataSource.kt new file mode 100644 index 00000000..846a6d24 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanLocalDataSource.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.repository + +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationDosageEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationNotificationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationScheduleEntityV1 +import de.gematik.ti.erp.app.db.writeOrCopyToRealm +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotificationMessage +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.prescription.repository.toRatio +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.types.RealmList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime + +private const val QUERY_TASK_ID = "taskId = $0" + +class MedicationPlanLocalDataSource( + private val realm: Realm +) { + fun loadMedicationSchedule(taskId: String): Flow = + realm.query( + QUERY_TASK_ID, + taskId + ).asFlow().map { + it.list.firstOrNull()?.toMedicationSchedule() + } + + suspend fun updateMedicationSchedule(medicationSchedule: MedicationSchedule) { + realm.writeOrCopyToRealm( + ::MedicationScheduleEntityV1, + QUERY_TASK_ID, + medicationSchedule.taskId + ) { entity -> + entity.profileId = medicationSchedule.profileId + entity.start = medicationSchedule.start.toString() + entity.isActive = medicationSchedule.isActive + entity.end = medicationSchedule.end.toString() + entity.title = medicationSchedule.message.title + entity.body = medicationSchedule.message.body + entity.taskId = medicationSchedule.taskId + entity.amount = medicationSchedule.amount?.toRatioEntity() + entity.notifications = medicationSchedule.notifications.toNotificationsEntity() + } + } + + fun loadAllMedicationSchedules(): Flow> = + realm.query().asFlow().map { + it.list.map { schedule -> + schedule.toMedicationSchedule() + } + } +} + +private fun MedicationScheduleEntityV1.toMedicationSchedule() = + MedicationSchedule( + taskId = taskId, + profileId = profileId, + start = LocalDate.parse(start), + end = LocalDate.parse(end), + amount = amount.toRatio(), + isActive = isActive, + message = MedicationNotificationMessage( + title = title, + body = body + ), + notifications = notifications.toNotifications() + ) + +private fun List.toNotificationsEntity(): RealmList = + this.sortedBy { it.time }.map { notification -> + MedicationNotificationEntityV1().apply { + this.id = notification.id + this.time = notification.time.toString() + this.dosage = MedicationDosageEntityV1().apply { + this.form = notification.dosage.form + this.ratio = notification.dosage.ratio + } + } + }.toRealmList() + +private fun RealmList.toNotifications(): List = + this.map { notification -> + MedicationNotification( + id = notification.id, + time = LocalTime.parse(notification.time), + dosage = MedicationDosage( + form = notification.dosage?.form ?: "", + ratio = notification.dosage?.ratio ?: "" + ) + ) + } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepository.kt new file mode 100644 index 00000000..3fc2eb35 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.repository + +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import kotlinx.coroutines.flow.Flow + +class MedicationPlanRepository( + private val localDataSource: MedicationPlanLocalDataSource +) { + fun loadMedicationSchedule(taskId: String): Flow = + localDataSource.loadMedicationSchedule(taskId) + + suspend fun updateMedicationSchedule(newMedicationSchedule: MedicationSchedule) = + localDataSource.updateMedicationSchedule(newMedicationSchedule) + + fun loadAllMedicationSchedules(): Flow> = + localDataSource.loadAllMedicationSchedules() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCase.kt new file mode 100644 index 00000000..398f631b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCase.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.model.parseInstruction +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge + +class GetDosageInstructionByTaskIdUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(taskId: String): Flow { + val synced = + repository + .loadSyncedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Synced) + .flowOn(dispatcher) + + val scanned = + repository + .loadScannedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Scanned) + .flowOn(dispatcher) + + return merge(synced, scanned).map { + when (it) { + is PrescriptionData.Synced -> parseInstruction(it.medicationRequest.dosageInstruction) + is PrescriptionData.Scanned -> MedicationPlanDosageInstruction.Empty + } + }.flowOn(dispatcher) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCase.kt new file mode 100644 index 00000000..9fa41228 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class LoadAllMedicationSchedulesUseCase( + private val medicationPlanRepository: MedicationPlanRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow> { + return medicationPlanRepository.loadAllMedicationSchedules().flowOn(dispatcher) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCase.kt new file mode 100644 index 00000000..6c2aafad --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class LoadMedicationScheduleByTaskIdUseCase( + private val medicationPlanRepository: MedicationPlanRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(taskId: String): Flow = + medicationPlanRepository.loadMedicationSchedule(taskId).flowOn(dispatcher) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCase.kt new file mode 100644 index 00000000..85d0cd56 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCase.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.ProfileWithSchedules +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalTime +import java.time.Duration + +class LoadProfilesWithSchedulesUseCase( + private val medicationPlanRepository: MedicationPlanRepository, + private val profileRepository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke( + now: LocalDateTime? = null + ): Flow> = combine( + medicationPlanRepository.loadAllMedicationSchedules(), + profileRepository.profiles() + ) { schedules, profiles -> + val filteredSchedules = schedules.filter { it.isActive } + if (now != null) { // profiles with active schedules sorted by closest notification + filteredSchedules.groupBy { it.profileId } + .map { (profileId, schedulesForProfile) -> + val profile = profiles.first { it.id == profileId } + val scheduleWithSortedNotifications = schedulesForProfile.map { schedule -> + schedule.copy( + notifications = sortNotificationsByClosestTime( + now, + schedule.notifications + ) + ) + } + ProfileWithSchedules( + profile.toModel(), + scheduleWithSortedNotifications + ) + }.sortedBy { profileWithSchedules -> + profileWithSchedules.medicationSchedules + .flatMap { it.notifications } + .minOf { notification -> + Duration.between( + now.time.toJavaLocalTime(), + notification.time.toJavaLocalTime() + ) + }.abs() + } + } else { // all profiles with active/inactive schedules + schedules.groupBy { it.profileId } + .map { (profileId, schedulesForProfile) -> + val profile = profiles.first { it.id == profileId } + ProfileWithSchedules( + profile.toModel(), + schedulesForProfile + ) + } + } + }.flowOn(dispatcher) + + private fun sortNotificationsByClosestTime( + now: LocalDateTime, + notifications: List + ): List { + val localTime = now.time + val first = notifications.minBy { + Duration.between(localTime.toJavaLocalTime(), it.time.toJavaLocalTime()).abs() + } + + val next = notifications.filter { it.id != first.id }.sortedBy { + val diff = Duration.between(localTime.toJavaLocalTime(), it.time.toJavaLocalTime()) + if (diff < Duration.ZERO) { + diff.plusDays(1) + } else { + diff + } + } + return listOf(first).plus(next) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationLocalDataSource.kt new file mode 100644 index 00000000..cb18cea4 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationLocalDataSource.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("SpreadOperator") + +package de.gematik.ti.erp.app.messages.repository +import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.toInstant +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.prescription.repository.toCommunication +import de.gematik.ti.erp.app.profiles.mapper.toProfileData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.github.aakira.napier.Napier +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.query.Sort +import io.realm.kotlin.query.max +import io.realm.kotlin.types.RealmInstant + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +class CommunicationLocalDataSource( + private val realm: Realm +) { + + fun loadDispReqCommunications( + orderId: String + ): Flow> = + realm.query( + "orderId = $0 && _profile = $1", + orderId, + CommunicationProfile.ErxCommunicationDispReq.toEntityValue() + ) + .asFlow() + .map { communication -> + communication.list.mapNotNull { + it.toCommunication() + } + } + + fun loadFirstDispReqCommunications( + profileId: ProfileIdentifier + ): Flow> = + realm.query( + "parent.parent.id = $0 && _profile = $1", + profileId, + CommunicationProfile.ErxCommunicationDispReq.toEntityValue() + ) + .sort("sentOn", Sort.DESCENDING) + .distinct("orderId") + .asFlow() + .map { communications -> + communications.list.mapNotNull { + it.toCommunication() + } + } + + fun loadRepliedCommunications( + taskIds: List, + telematikId: String? = null // To handle the optional case + ): Flow> { + var query = realm.query( + orQuerySubstring("parent.taskId", taskIds.size), + *taskIds.toTypedArray() + ) + .query("_profile = $0", CommunicationProfile.ErxCommunicationReply.toEntityValue()) + .sort("sentOn", Sort.DESCENDING) + + if (!telematikId.isNullOrEmpty()) { + query = query.query("sender = $0", telematikId) + } + + return query.asFlow().map { results -> + val distinctCommunications = results.list + .mapNotNull { it.toCommunication() } + .distinctBy { it.payload } // Distinct payloads for duplicate reply messages + + distinctCommunications + } + } + + fun hasUnreadDispenseMessage(taskIds: List, orderId: String): Flow = + realm.query( + orQuerySubstring("parent.taskId", taskIds.size), + *taskIds.toTypedArray() + ) + .query("consumed = false && orderId = $0", orderId) + .count() + .asFlow() + .map { it > 0 } + + fun hasUnreadDispenseMessage(profileId: ProfileIdentifier): Flow = + realm.query("consumed = false && parent.parent.id = $0", profileId) + .count() + .asFlow() + .map { it > 0 } + + /** + * [consumed] which refers to if the order message was read is checked to be false. + * @return [flow] of the count of the unread messages + */ + fun unreadMessagesCount(consumed: Boolean = false): Flow = + realm.query("consumed = $consumed") + .asFlow() + .map { results -> + val messages = results.list + + val dispReqMessages = messages.filter { it.profile == CommunicationProfileV1.ErxCommunicationDispReq } + val uniqueDispReqOrders = dispReqMessages.distinctBy { it.orderId } // Count only distinct orders + + val replyMessages = messages.filter { it.profile == CommunicationProfileV1.ErxCommunicationReply } + val uniqueReplies = replyMessages.distinctBy { it.taskId to it.payload to it.sender } + + val dispReqCount = uniqueDispReqOrders.size + val uniqueReplyCount = uniqueReplies.size + + val totalCount = dispReqCount + uniqueReplyCount + + totalCount.toLong() + } + + fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = + realm.query("consumed = false && parent.parent.id = $0", profileId) + .count() + .asFlow() + + private fun orQuerySubstring(field: String, count: Int): String = + (0 until count) + .map { "$field = $$it" } + .joinToString(" || ") + + fun taskIdsByOrder(orderId: String): Flow> = + realm.query( + "orderId = $0", + orderId + ).distinct("taskId") + .asFlow() + .map { result -> + val taskIds = result.list.map { it.taskId } + Napier.d("Retrieved taskIDs: $taskIds") + taskIds + } + + fun getProfileByOrderId(orderId: String): Flow = + realm.query("orderId = $0", orderId) + .first() + .asFlow() + .mapNotNull { + it.obj?.parent?.parent?.toProfileData() + } + + suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { + realm.write { + val originalCommunication = queryFirst("communicationId = $0", communicationId) + originalCommunication?.let { communication -> + + val communicationsToUpdate = query( + "orderId = $0 && taskId = $1 && payload = $2 && sender = $3 && recipient = $4", + communication.orderId, + communication.taskId, + communication.payload, + communication.sender, + communication.recipient + ).find() + + communicationsToUpdate.forEach { it.consumed = consumed } + } + } + } + + fun latestCommunicationTimestamp(profileId: ProfileIdentifier) = + realm.query("parent.parent.id = $0", profileId) + .max("sentOn") + .asFlow() + .map { it?.toInstant() } + + fun hasUnreadRepliedMessages(taskIds: List, telematikId: String?) = + realm.query( + orQuerySubstring("parent.taskId", taskIds.size), + *taskIds.toTypedArray() + ) + .query("consumed = false") + .query("_profile = $0", CommunicationProfile.ErxCommunicationReply.toEntityValue()) + .query("sender = $0", telematikId) + .count() + .asFlow() + .map { it > 0 } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationRepository.kt new file mode 100644 index 00000000..da26f9ce --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/CommunicationRepository.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.repository + +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + +@Suppress("TooManyFunctions") +interface CommunicationRepository { + + val pharmacyCacheError: Channel + val pharmacyDownloaded: Channel + + suspend fun downloadCommunications(profileId: ProfileIdentifier): Result + suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> + + suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? + fun loadPharmacies(): Flow> + suspend fun downloadMissingPharmacy(telematikId: String): Result + fun loadSyncedByTaskId(taskId: String): Flow + fun loadScannedByTaskId(taskId: String): Flow + + fun loadDispReqCommunications(orderId: String): Flow> + fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> + fun loadRepliedCommunications(taskIds: List, telematikId: String): Flow> + fun hasUnreadDispenseMessage(taskIds: List, orderId: String): Flow + fun hasUnreadDispenseMessage(profileId: ProfileIdentifier): Flow + fun unreadMessagesCount(consumed: Boolean): Flow + fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow + fun taskIdsByOrder(orderId: String): Flow> + fun profileByOrderId(orderId: String): Flow + suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) + suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) + suspend fun hasUnreadRepliedMessages(taskIds: List, telematikId: String): Flow +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/DefaultCommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/DefaultCommunicationRepository.kt new file mode 100644 index 00000000..913fe413 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/DefaultCommunicationRepository.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.repository + +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.datetime.Instant +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.resumeWithException + +private const val COMMUNICATION_MAX_PAGE_SIZE = 50 + +@Suppress("TooManyFunctions") +class DefaultCommunicationRepository( + private val taskLocalDataSource: PrescriptionLocalDataSource, + private val taskRemoteDataSource: PrescriptionRemoteDataSource, + private val communicationLocalDataSource: CommunicationLocalDataSource, + private val cacheLocalDataSource: PharmacyCacheLocalDataSource, + private val cacheRemoteDataSource: PharmacyCacheRemoteDataSource, + private val dispatchers: DispatchProvider +) : ResourcePaging(dispatchers, COMMUNICATION_MAX_PAGE_SIZE), CommunicationRepository { + private val scope = CoroutineScope(dispatchers.io) // todo: forced scope, not testable + private val queue = Channel(capacity = Channel.BUFFERED) + + override val pharmacyCacheError = Channel() + override val pharmacyDownloaded = Channel() + + init { + scope.launch { + for (telematikId in queue) { + cacheRemoteDataSource + .searchPharmacy(telematikId) + .onSuccess { + val pharmacy = extractPharmacyServices(it).pharmacies.firstOrNull() + pharmacy?.let { + cacheLocalDataSource.savePharmacy(pharmacy.telematikId, pharmacy.name) + pharmacyDownloaded.send(pharmacy) + } ?: run { + Napier.e("Pharmacy not found for telematikId $telematikId") + pharmacyDownloaded.send(null) + } + } + .onFailure { + Napier.e("Failed to download pharmacy for cache with telematikId $telematikId", it) + pharmacyCacheError.send(it) + } + } + } + } + + override val tag: String = "CommunicationRepository" + + override suspend fun downloadCommunications(profileId: ProfileIdentifier) = downloadPaged(profileId) + + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> = + taskRemoteDataSource.fetchCommunications( + profileId = profileId, + count = count, + lastKnownUpdate = timestamp + ).mapCatching { communications -> + Napier.i("Communication Json: $communications") + taskLocalDataSource.saveCommunications(communications) + }.map { + // we get the count of communications, but we don't use it?? + ResourceResult(it, Unit) + } + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + communicationLocalDataSource.latestCommunicationTimestamp(profileId).first() + + override fun loadPharmacies(): Flow> = + cacheLocalDataSource.loadPharmacies().flowOn(dispatchers.io) + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun downloadMissingPharmacy(telematikId: String): Result { + queue.send(telematikId) + return suspendCancellableCoroutine { continuation -> + val resumed = AtomicBoolean(false) // Flag to track if continuation is already resumed + // Launch a coroutine to listen for the first emit of success or error + val successJob = scope.launch { + try { + val pharmacy = pharmacyDownloaded.receive() + if (resumed.compareAndSet(false, true)) { + continuation.resume(Result.success(pharmacy?.toCachedPharmacy()), onCancellation = null) + } + } catch (e: Throwable) { + if (resumed.compareAndSet(false, true)) { + continuation.resumeWithException(e) + } + } + } + + val errorJob = scope.launch { + val result = pharmacyCacheError.receive() + try { + if (resumed.compareAndSet(false, true)) { + continuation.resume(Result.failure(result), onCancellation = null) + } + } catch (e: Throwable) { + if (resumed.compareAndSet(false, true)) { + continuation.resumeWithException(e) + } + } + } + + // If the coroutine is cancelled, cancel the jobs as well + continuation.invokeOnCancellation { + successJob.cancel() + errorJob.cancel() + } + } + } + + override fun loadSyncedByTaskId(taskId: String): Flow = + taskLocalDataSource.loadSyncedTaskByTaskId(taskId) + .flowOn(dispatchers.io) + + override fun loadScannedByTaskId(taskId: String): Flow = + taskLocalDataSource.loadScannedTaskByTaskId(taskId) + .flowOn(dispatchers.io) + + override fun loadDispReqCommunications(orderId: String): Flow> = + communicationLocalDataSource.loadDispReqCommunications(orderId).flowOn(dispatchers.io) + + override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> = + communicationLocalDataSource.loadFirstDispReqCommunications(profileId).flowOn(dispatchers.io) + + override fun loadRepliedCommunications(taskIds: List, telematikId: String): Flow> = + communicationLocalDataSource.loadRepliedCommunications( + taskIds = taskIds, + telematikId = telematikId + ).flowOn(dispatchers.io) + + override fun hasUnreadDispenseMessage(taskIds: List, orderId: String): Flow = + communicationLocalDataSource.hasUnreadDispenseMessage(taskIds, orderId).flowOn(dispatchers.io) + + override fun hasUnreadDispenseMessage(profileId: ProfileIdentifier): Flow = + communicationLocalDataSource.hasUnreadDispenseMessage(profileId).flowOn(dispatchers.io) + + override fun unreadMessagesCount(consumed: Boolean): Flow = + communicationLocalDataSource.unreadMessagesCount(consumed) + + override fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = + communicationLocalDataSource.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.io) + + override fun taskIdsByOrder(orderId: String): Flow> = + communicationLocalDataSource.taskIdsByOrder(orderId).flowOn(dispatchers.io) + + override fun profileByOrderId(orderId: String): Flow { + return communicationLocalDataSource.getProfileByOrderId(orderId) + } + + override suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { + communicationLocalDataSource.setCommunicationStatus(communicationId, consumed) + } + + override suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { + taskLocalDataSource.saveLocalCommunication(taskId, pharmacyId, transactionId) + } + + override suspend fun hasUnreadRepliedMessages(taskIds: List, telematikId: String): Flow = + communicationLocalDataSource.hasUnreadRepliedMessages(taskIds, telematikId) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheLocalDataSource.kt new file mode 100644 index 00000000..15683fba --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheLocalDataSource.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.repository + +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.PharmacyCacheEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.fhir.model.Pharmacy +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +class PharmacyCacheLocalDataSource( + private val realm: Realm +) { + fun loadPharmacies() = + realm + .query() + .asFlow() + .map { result -> + result.list.map { + it.toCachedPharmacy() + } + } + .distinctUntilChanged() + + suspend fun savePharmacy(telematikId: String, name: String) { + realm.write { + realm.queryFirst("telematikId = $0", telematikId)?.apply { + this.name = name + } ?: run { + copyToRealm( + PharmacyCacheEntityV1().apply { + this.telematikId = telematikId + this.name = name + } + ) + } + } + } +} + +data class CachedPharmacy( + val name: String, + val telematikId: String +) + +fun PharmacyCacheEntityV1.toCachedPharmacy() = + CachedPharmacy( + name = this.name, + telematikId = this.telematikId + ) + +fun Pharmacy.toCachedPharmacy() = + CachedPharmacy( + name = this.name, + telematikId = this.telematikId + ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheRemoteDataSource.kt new file mode 100644 index 00000000..91f91c00 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/messages/repository/PharmacyCacheRemoteDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.messages.repository + +import de.gematik.ti.erp.app.api.PharmacySearchService +import de.gematik.ti.erp.app.api.safeApiCall +import kotlinx.serialization.json.JsonElement + +class PharmacyCacheRemoteDataSource( + private val searchService: PharmacySearchService +) { + suspend fun searchPharmacy( + telematikId: String + ): Result = safeApiCall("error searching pharmacy by telematikId") { + if (telematikId.startsWith("3-SMC")) { + searchService.search(names = listOf(telematikId), emptyMap()) + } else { + searchService.searchByTelematikId(telematikId = telematikId) + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt index 529b4f19..94cb557b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.navigation @@ -78,10 +78,11 @@ inline fun fromNavigationString(value: String): T { } catch (e: SerializationException) { Napier.e { """ - Check if value classes are used. - [kotlinx.serialization] does not work - well for polymorphic serialization with value classes. - Try changing them to data classes. + (1) If sealed classes and sealed interfaces are used, + please check if the correct PolymorphicSerializer is used. + (2) Check if value classes are used. + [kotlinx.serialization] does not work well for polymorphic + serialization with value classes. Try changing them to data classes. """.trimIndent() } throw e diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt deleted file mode 100644 index 672fc524..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("SpreadOperator") - -package de.gematik.ti.erp.app.orders.repository - -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 -import de.gematik.ti.erp.app.db.queryFirst -import de.gematik.ti.erp.app.db.toInstant -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.model.CommunicationProfile -import de.gematik.ti.erp.app.prescription.repository.toCommunication -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query -import io.realm.kotlin.query.Sort -import io.realm.kotlin.query.max -import io.realm.kotlin.types.RealmInstant -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class CommunicationLocalDataSource( - private val realm: Realm -) { - - fun loadDispReqCommunications( - orderId: String - ): Flow> = - realm.query( - "orderId = $0 && _profile = $1", - orderId, - CommunicationProfile.ErxCommunicationDispReq.toEntityValue() - ) - .asFlow() - .map { communication -> - communication.list.mapNotNull { - it.toCommunication() - } - } - - fun loadFirstDispReqCommunications( - profileId: ProfileIdentifier - ): Flow> = - realm.query( - "parent.parent.id = $0 && _profile = $1", - profileId, - CommunicationProfile.ErxCommunicationDispReq.toEntityValue() - ) - .sort("sentOn", Sort.DESCENDING) - .distinct("orderId") - .asFlow() - .map { communications -> - communications.list.mapNotNull { - it.toCommunication() - } - } - - fun loadRepliedCommunications( - taskIds: List - ): Flow> = - realm.query( - orQuerySubstring("parent.taskId", taskIds.size), - *taskIds.toTypedArray() - ) - .query("_profile = $0", CommunicationProfile.ErxCommunicationReply.toEntityValue()) - .sort("sentOn", Sort.DESCENDING) - .distinct("payload") - .asFlow() - .map { communications -> - communications.list.mapNotNull { - it.toCommunication() - } - } - - fun loadCommunicationsWithTaskId( - taskIds: List - ): Flow> = - realm.query( - orQuerySubstring("parent.taskId", taskIds.size), - *taskIds.toTypedArray() - ) - .sort("sentOn", Sort.DESCENDING) - .distinct("orderId") - .asFlow() - .map { communications -> - communications.list.mapNotNull { - it.toCommunication() - } - } - - fun hasUnreadPrescription(taskIds: List, orderId: String): Flow = - realm.query( - orQuerySubstring("parent.taskId", taskIds.size), - *taskIds.toTypedArray() - ) - .query("consumed = false && orderId = $0", orderId) - .count() - .asFlow() - .map { it > 0 } - - fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow = - realm.query("consumed = false && parent.parent.id = $0", profileId) - .count() - .asFlow() - .map { it > 0 } - - /** - * @param profileId is used to check for a particular profile. - * - * [[consumed]] which refers to if the order message was read is checked to be false. - * - * [[_profile]] which refers to reply or request reads only if there are request message. - * - * @return [flow] of the count of the unread messages - */ - fun unreadOrders(profileId: ProfileIdentifier): Flow = - realm.query( - "consumed = false && parent.parent.id = $0 && _profile = $1", - profileId, - CommunicationProfile.ErxCommunicationDispReq.toEntityValue() - ) - .distinct("orderId") - .count() - .asFlow() - - fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = - realm.query("consumed = false && parent.parent.id = $0", profileId) - .count() - .asFlow() - - private fun orQuerySubstring(field: String, count: Int): String = - (0 until count) - .map { "$field = $$it" } - .joinToString(" || ") - - fun taskIdsByOrder(orderId: String): Flow> = - realm.query( - "orderId = $0 && _profile = $1", - orderId, - CommunicationProfile.ErxCommunicationDispReq.toEntityValue() - ) - .asFlow() - .map { result -> - result.list.map { it.taskId } - } - - suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { - realm.write { - queryFirst("communicationId = $0", communicationId)?.apply { - this.consumed = consumed - } - } - } - - fun latestCommunicationTimestamp(profileId: ProfileIdentifier) = - realm.query("parent.parent.id = $0", profileId) - .max("sentOn") - .asFlow() - .map { - it?.toInstant() - } -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt deleted file mode 100644 index acf28485..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.repository - -import de.gematik.ti.erp.app.api.ResourcePaging -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.model.ScannedTaskData -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.datetime.Instant - -@Suppress("TooManyFunctions") -interface CommunicationRepository { - - val pharmacyCacheError: MutableSharedFlow - suspend fun downloadCommunications(profileId: ProfileIdentifier): Result - suspend fun downloadResource( - profileId: ProfileIdentifier, - timestamp: String?, - count: Int? - ): Result> - - suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? - fun loadPharmacies(): Flow> - suspend fun downloadMissingPharmacy(telematikId: String) - fun loadSyncedByTaskId(taskId: String): Flow - fun loadScannedByTaskId(taskId: String): Flow - - fun loadDispReqCommunications(orderId: String): Flow> - fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> - fun loadRepliedCommunications(taskIds: List): Flow> - fun loadCommunicationsWithTaskId(taskIds: List): Flow> - fun hasUnreadPrescription(taskIds: List, orderId: String): Flow - fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow - fun unreadOrders(profileId: ProfileIdentifier): Flow - fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow - fun taskIdsByOrder(orderId: String): Flow> - suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) - suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt deleted file mode 100644 index 7fa66ea6..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.repository - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.api.ResourcePaging -import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices -import de.gematik.ti.erp.app.prescription.model.Communication -import de.gematik.ti.erp.app.prescription.model.ScannedTaskData -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import io.github.aakira.napier.Napier -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.datetime.Instant - -private const val COMMUNICATION_MAX_PAGE_SIZE = 50 - -@Suppress("TooManyFunctions") -class DefaultCommunicationRepository( - private val taskLocalDataSource: PrescriptionLocalDataSource, - private val taskRemoteDataSource: PrescriptionRemoteDataSource, - private val communicationLocalDataSource: CommunicationLocalDataSource, - private val cacheLocalDataSource: PharmacyCacheLocalDataSource, - private val cacheRemoteDataSource: PharmacyCacheRemoteDataSource, - private val dispatchers: DispatchProvider -) : ResourcePaging(dispatchers, COMMUNICATION_MAX_PAGE_SIZE), CommunicationRepository { - private val scope = CoroutineScope(dispatchers.io) - private val queue = Channel(capacity = Channel.BUFFERED) - - override val pharmacyCacheError = MutableSharedFlow() - - init { - scope.launch { - for (telematikId in queue) { - cacheRemoteDataSource - .searchPharmacy(telematikId) - .onSuccess { - val pharmacy = extractPharmacyServices(it).pharmacies.firstOrNull() - pharmacy?.let { - cacheLocalDataSource.savePharmacy(pharmacy.telematikId, pharmacy.name) - } - } - .onFailure { - Napier.e("Failed to download pharmacy for cache with telematikId $telematikId", it) - pharmacyCacheError.tryEmit(it) - } - } - } - } - - override val tag: String = "CommunicationRepository" - - override suspend fun downloadCommunications(profileId: ProfileIdentifier) = downloadPaged(profileId) - - override suspend fun downloadResource( - profileId: ProfileIdentifier, - timestamp: String?, - count: Int? - ): Result> = - taskRemoteDataSource.fetchCommunications( - profileId = profileId, - count = count, - lastKnownUpdate = timestamp - ).mapCatching { communications -> - taskLocalDataSource.saveCommunications(communications) - }.map { - ResourceResult(it, Unit) - } - - override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = - communicationLocalDataSource.latestCommunicationTimestamp(profileId).first() - - override fun loadPharmacies(): Flow> = - cacheLocalDataSource.loadPharmacies().flowOn(dispatchers.io) - - override suspend fun downloadMissingPharmacy(telematikId: String) { - queue.send(telematikId) - } - - override fun loadSyncedByTaskId(taskId: String): Flow = - taskLocalDataSource.loadSyncedTaskByTaskId(taskId) - .flowOn(dispatchers.io) - - override fun loadScannedByTaskId(taskId: String): Flow = - taskLocalDataSource.loadScannedTaskByTaskId(taskId) - .flowOn(dispatchers.io) - - override fun loadDispReqCommunications(orderId: String): Flow> = - communicationLocalDataSource.loadDispReqCommunications(orderId).flowOn(dispatchers.io) - - override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> = - communicationLocalDataSource.loadFirstDispReqCommunications(profileId).flowOn(dispatchers.io) - - override fun loadRepliedCommunications(taskIds: List): Flow> = - communicationLocalDataSource.loadRepliedCommunications(taskIds = taskIds).flowOn(dispatchers.io) - - override fun loadCommunicationsWithTaskId(taskIds: List): Flow> = - communicationLocalDataSource.loadCommunicationsWithTaskId(taskIds = taskIds).flowOn(dispatchers.io) - - override fun hasUnreadPrescription(taskIds: List, orderId: String): Flow = - communicationLocalDataSource.hasUnreadPrescription(taskIds, orderId).flowOn(dispatchers.io) - - override fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow = - communicationLocalDataSource.hasUnreadPrescription(profileId).flowOn(dispatchers.io) - - override fun unreadOrders(profileId: ProfileIdentifier): Flow = - communicationLocalDataSource.unreadOrders(profileId).flowOn(dispatchers.io) - override fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = - communicationLocalDataSource.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.io) - - override fun taskIdsByOrder(orderId: String): Flow> = - communicationLocalDataSource.taskIdsByOrder(orderId).flowOn(dispatchers.io) - - override suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { - withContext(dispatchers.io) { - communicationLocalDataSource.setCommunicationStatus(communicationId, consumed) - } - } - - override suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { - taskLocalDataSource.saveLocalCommunication(taskId, pharmacyId, transactionId) - } -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt deleted file mode 100644 index c4fedc4c..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.repository - -import de.gematik.ti.erp.app.db.entities.v1.pharmacy.PharmacyCacheEntityV1 -import de.gematik.ti.erp.app.db.queryFirst -import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -class PharmacyCacheLocalDataSource( - private val realm: Realm -) { - fun loadPharmacies() = - realm - .query() - .asFlow() - .map { result -> - result.list.map { - it.toCachedPharmacy() - } - } - .distinctUntilChanged() - - suspend fun savePharmacy(telematikId: String, name: String) { - realm.write { - realm.queryFirst("telematikId = $0", telematikId)?.apply { - this.name = name - } ?: run { - copyToRealm( - PharmacyCacheEntityV1().apply { - this.telematikId = telematikId - this.name = name - } - ) - } - } - } -} - -data class CachedPharmacy( - val name: String, - val telematikId: String -) - -fun PharmacyCacheEntityV1.toCachedPharmacy() = - CachedPharmacy( - name = this.name, - telematikId = this.telematikId - ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt deleted file mode 100644 index 56e342ce..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.orders.repository - -import de.gematik.ti.erp.app.api.PharmacySearchService -import de.gematik.ti.erp.app.api.safeApiCall -import kotlinx.serialization.json.JsonElement - -class PharmacyCacheRemoteDataSource( - private val searchService: PharmacySearchService -) { - suspend fun searchPharmacy( - telematikId: String - ): Result = safeApiCall("error searching pharmacy by telematikId") { - if (telematikId.startsWith("3-SMC")) { - searchService.search(names = listOf(telematikId), emptyMap()) - } else { - searchService.searchByTelematikId(telematikId = telematikId) - } - } -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunication.kt index 90c97d0c..11f02f2e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunication.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunication.kt @@ -1,25 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy import de.gematik.ti.erp.app.BCProvider import de.gematik.ti.erp.app.Requirement +import io.github.aakira.napier.Napier import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1PrintableString @@ -43,15 +44,14 @@ import org.bouncycastle.cms.jcajce.JceCMSContentEncryptorBuilder import org.bouncycastle.cms.jcajce.JceKeyTransRecipientInfoGenerator import org.bouncycastle.operator.OutputAEADEncryptor import org.bouncycastle.operator.jcajce.JceAsymmetricKeyWrapper -import io.github.aakira.napier.Napier import java.security.spec.MGF1ParameterSpec import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.PSource const val OidRecipientMail = "1.2.276.0.76.4.173" // komle-recipient-emails @Requirement( - "A_22778#2", - "A_22779#2", + "A_22778-01#3", + "A_22779-01#3", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Build and encrypt direct redeem message with pharmacy`s certificate" ) @@ -83,7 +83,7 @@ fun buildDirectPharmacyMessage( } @Requirement( "GS-A_4389#3", - "GS-A_4390", + "GS-A_4390#1", sourceSpecification = "gemSpec_Krypt", rationale = "Build and encrypt direct redeem message with pharmacy`s certificate" ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt index f81b6f8f..83d6230c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyScreenData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyScreenData.kt index 03588083..887e501e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyScreenData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/model/PharmacyScreenData.kt @@ -1,26 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.model import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable +// TODO: Poor naming we need to change object PharmacyScreenData { + @Serializable @Immutable enum class OrderOption { PickupService, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt deleted file mode 100644 index 2ead9e05..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.repository - -import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.pharmacy.OftenUsedPharmacyEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 -import de.gematik.ti.erp.app.db.queryFirst -import de.gematik.ti.erp.app.db.toInstant -import de.gematik.ti.erp.app.db.toRealmInstant -import de.gematik.ti.erp.app.db.tryWrite -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy -import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query -import io.realm.kotlin.query.Sort -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.datetime.Clock - -class DefaultPharmacyLocalDataSource(private val realm: Realm) : PharmacyLocalDataSource { - override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { - delete(it) - } - queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { - delete(it) - } - } - } - - override fun loadOftenUsedPharmacies(): Flow> = - realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { - it.list.map { pharmacy -> - pharmacy.toOverviewPharmacy() - } - } - - override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Clock.System.now().toRealmInstant() - this.usageCount += 1 - } ?: copyToRealm(pharmacy.toOftenUsedPharmacyEntityV1()) - } - } - - override suspend fun deleteFavoritePharmacy(favoritePharmacy: Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", favoritePharmacy.telematikId)?.let { delete(it) } - } - } - - override fun loadFavoritePharmacies(): Flow> = - realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { - it.list.map { favorite -> - favorite.toOverviewPharmacy() - } - } - - override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Clock.System.now().toRealmInstant() - } ?: copyToRealm(pharmacy.toFavoritePharmacyEntityV1()) - } - } - - override fun isPharmacyInFavorites(pharmacy: Pharmacy): Flow = - realm.query("telematikId = $0", pharmacy.telematikId) - .asFlow() - .map { - it.list.isNotEmpty() - } - - override suspend fun markAsRedeemed(taskId: String) { - realm.tryWrite { - queryFirst("taskId = $0", taskId)?.apply { - this.redeemedOn = Clock.System.now().toRealmInstant() - } - } - } - - companion object { - fun Pharmacy.toOftenUsedPharmacyEntityV1() = - OftenUsedPharmacyEntityV1().apply { - this.address = this@toOftenUsedPharmacyEntityV1.singleLineAddress() - this.pharmacyName = this@toOftenUsedPharmacyEntityV1.name - this.telematikId = this@toOftenUsedPharmacyEntityV1.telematikId - } - - fun OftenUsedPharmacyEntityV1.toOverviewPharmacy() = - OverviewPharmacy( - lastUsed = this.lastUsed.toInstant(), - usageCount = this.usageCount, - isFavorite = false, - telematikId = this.telematikId, - pharmacyName = this.pharmacyName, - address = this.address - ) - - fun Pharmacy.toFavoritePharmacyEntityV1() = - FavoritePharmacyEntityV1().apply { - this.address = this@toFavoritePharmacyEntityV1.singleLineAddress() - this.pharmacyName = this@toFavoritePharmacyEntityV1.name - this.telematikId = this@toFavoritePharmacyEntityV1.telematikId - } - - fun FavoritePharmacyEntityV1.toOverviewPharmacy() = - OverviewPharmacy( - lastUsed = this.lastUsed.toInstant(), - telematikId = this.telematikId, - pharmacyName = this.pharmacyName, - address = this.address, - isFavorite = true, - usageCount = 0 - ) - } -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt index ad5e00d1..f598155d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt @@ -1,88 +1,80 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.repository -import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.fhir.model.PharmacyServices import de.gematik.ti.erp.app.fhir.model.extractBinaryCertificatesAsBase64 import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.datasource.FavouritePharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.OftenUsedPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.datasource.PharmacyRemoteDataSource import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.redeem.repository.datasource.RedeemLocalDataSource import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.withContext class DefaultPharmacyRepository( private val remoteDataSource: PharmacyRemoteDataSource, - private val localDataSource: PharmacyLocalDataSource, - private val dispatchers: DispatchProvider + private val redeemLocalDataSource: RedeemLocalDataSource, + private val favouriteLocalDataSource: FavouritePharmacyLocalDataSource, + private val oftenUsedLocalDataSource: OftenUsedPharmacyLocalDataSource ) : PharmacyRepository { override suspend fun searchPharmacies( names: List, filter: Map - ): Result = - remoteDataSource.searchPharmacies(names, filter) - .map { jsonElement -> - extractPharmacyServices( - bundle = jsonElement, - onError = { it, cause -> - Napier.e(cause) { - it.toString() - } + ): Result = remoteDataSource.searchPharmacies(names, filter) + .map { jsonElement -> + extractPharmacyServices( + bundle = jsonElement, + onError = { it, cause -> + Napier.e(cause) { + it.toString() } - ) - } + } + ) + } override suspend fun searchPharmaciesByBundle( bundleId: String, offset: Int, count: Int - ): Result = - withContext(dispatchers.io) { - remoteDataSource.searchPharmaciesContinued( - bundleId = bundleId, - offset = offset, - count = count - ).map { - extractPharmacyServices( - bundle = it, - onError = { element, cause -> - Napier.e(cause) { - element.toString() - } - } - ) + ): Result = remoteDataSource.searchPharmaciesContinued( + bundleId = bundleId, + offset = offset, + count = count + ).map { + extractPharmacyServices( + bundle = it, + onError = { element, cause -> + Napier.e(cause) { + element.toString() + } } - } + ) + } override suspend fun searchBinaryCerts( locationId: String - ): Result> = - withContext(dispatchers.io) { - remoteDataSource.searchBinaryCert( - locationId = locationId - ).map { - extractBinaryCertificatesAsBase64( - bundle = it - ) - } + ): Result> = remoteDataSource.searchBinaryCert(locationId = locationId) + .map { + extractBinaryCertificatesAsBase64(bundle = it) } override suspend fun redeemPrescriptionDirectly( @@ -90,63 +82,49 @@ class DefaultPharmacyRepository( message: ByteArray, pharmacyTelematikId: String, transactionId: String - ): Result = - remoteDataSource.redeemPrescriptionDirectly( - url = url, - message = message, - pharmacyTelematikId = pharmacyTelematikId, - transactionId = transactionId - ) + ): Result = remoteDataSource.redeemPrescriptionDirectly( + url = url, + message = message, + pharmacyTelematikId = pharmacyTelematikId, + transactionId = transactionId + ) - override fun loadOftenUsedPharmacies() = - localDataSource.loadOftenUsedPharmacies().flowOn(dispatchers.io) + override fun loadOftenUsedPharmacies() = oftenUsedLocalDataSource.loadOftenUsedPharmacies() - override fun loadFavoritePharmacies() = - localDataSource.loadFavoritePharmacies().flowOn(dispatchers.io) + override fun loadFavoritePharmacies() = favouriteLocalDataSource.loadFavoritePharmacies() - override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.io) { - localDataSource.saveOrUpdateOftenUsedPharmacy(pharmacy) - } + override suspend fun markPharmacyAsOftenUsed(pharmacy: PharmacyUseCaseData.Pharmacy) { + oftenUsedLocalDataSource.markPharmacyAsOftenUsed(pharmacy) } override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - withContext(dispatchers.io) { - localDataSource.deleteOverviewPharmacy(overviewPharmacy) - } + oftenUsedLocalDataSource.deleteOverviewPharmacy(overviewPharmacy) } - override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.io) { - localDataSource.saveOrUpdateFavoritePharmacy(pharmacy) - } + override suspend fun markPharmacyAsFavourite(pharmacy: PharmacyUseCaseData.Pharmacy) { + favouriteLocalDataSource.markPharmacyAsFavourite(pharmacy) } override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.io) { - localDataSource.deleteFavoritePharmacy(favoritePharmacy) - } + favouriteLocalDataSource.deleteFavoritePharmacy(favoritePharmacy) } override suspend fun searchPharmacyByTelematikId( telematikId: String - ): Result = - withContext(dispatchers.io) { - remoteDataSource.searchPharmacyByTelematikId(telematikId) - .map { - extractPharmacyServices( - bundle = it, - onError = { element, cause -> - Napier.e(element.toString(), cause) - } - ) + ): Result = remoteDataSource.searchPharmacyByTelematikId(telematikId) + .map { + extractPharmacyServices( + bundle = it, + onError = { element, cause -> + Napier.e(element.toString(), cause) } + ) } override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = - localDataSource.isPharmacyInFavorites(pharmacy).flowOn(dispatchers.io) + favouriteLocalDataSource.isPharmacyInFavorites(pharmacy) override suspend fun markAsRedeemed(taskId: String) { - localDataSource.markAsRedeemed(taskId) + redeemLocalDataSource.markAsRedeemed(taskId) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultShippingContactRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultShippingContactRepository.kt new file mode 100644 index 00000000..41a63090 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultShippingContactRepository.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository + +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.ShippingContactEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +class DefaultShippingContactRepository( + private val dispatchers: DispatchProvider, + private val realm: Realm +) : ShippingContactRepository { + override fun shippingContact(): Flow = + realm.query() + .first() + .asFlow() + .map { + it.obj?.toShippingContact() + } + .flowOn(dispatchers.io) + + override suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) { + withContext(dispatchers.io) { + realm.write { + queryFirst()?.let { settings -> + val shippingContact = settings.shippingContact + ?: copyToRealm(ShippingContactEntityV1()).also { + settings.shippingContact = it + } + + shippingContact.let { + it.address!!.line1 = contact.line1 + it.address!!.line2 = contact.line2 + it.address!!.postalCode = contact.postalCode + it.address!!.city = contact.city + it.name = contact.name + it.telephoneNumber = contact.telephoneNumber + it.mail = contact.mail + it.deliveryInformation = contact.deliveryInformation + } + } + } + } + } +} + +fun ShippingContactEntityV1.toShippingContact() = + PharmacyData.ShippingContact( + name = this.name, + line1 = this.address!!.line1, + line2 = this.address!!.line2, + postalCode = this.address!!.postalCode, + city = this.address!!.city, + telephoneNumber = this.telephoneNumber, + mail = this.mail, + deliveryInformation = this.deliveryInformation + ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt deleted file mode 100644 index 66d108ee..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.repository - -import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import kotlinx.coroutines.flow.Flow - -interface PharmacyLocalDataSource { - suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) - fun loadOftenUsedPharmacies(): Flow> - suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) - suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) - fun loadFavoritePharmacies(): Flow> - suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) - fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow - suspend fun markAsRedeemed(taskId: String) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt index eb5a6bd5..1c0dc673 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt @@ -1,28 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("NewLineAtEndOfFile") package de.gematik.ti.erp.app.pharmacy.repository -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.fhir.model.PharmacyServices import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import kotlinx.coroutines.flow.Flow interface PharmacyRepository { @@ -52,11 +52,11 @@ interface PharmacyRepository { fun loadFavoritePharmacies(): Flow> - suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) + suspend fun markPharmacyAsOftenUsed(pharmacy: PharmacyUseCaseData.Pharmacy) suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) - suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) + suspend fun markPharmacyAsFavourite(pharmacy: PharmacyUseCaseData.Pharmacy) suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt index cff87151..049d9e3b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt @@ -1,81 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.repository -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.ShippingContactEntityV1 -import de.gematik.ti.erp.app.db.queryFirst import de.gematik.ti.erp.app.pharmacy.model.PharmacyData -import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -class ShippingContactRepository( - private val dispatchers: DispatchProvider, - private val realm: Realm -) { - fun shippingContact(): Flow = - realm.query() - .first() - .asFlow() - .map { - it.obj?.toShippingContact() - } - .flowOn(dispatchers.io) +interface ShippingContactRepository { - suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) { - withContext(dispatchers.io) { - realm.write { - queryFirst()?.let { settings -> - val shippingContact = settings.shippingContact - ?: copyToRealm(ShippingContactEntityV1()).also { - settings.shippingContact = it - } + fun shippingContact(): Flow - shippingContact.let { - it.address!!.line1 = contact.line1 - it.address!!.line2 = contact.line2 - it.address!!.postalCode = contact.postalCode - it.address!!.city = contact.city - it.name = contact.name - it.telephoneNumber = contact.telephoneNumber - it.mail = contact.mail - it.deliveryInformation = contact.deliveryInformation - } - } - } - } - } + suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) } - -fun ShippingContactEntityV1.toShippingContact() = - PharmacyData.ShippingContact( - name = this.name, - line1 = this.address!!.line1, - line2 = this.address!!.line2, - postalCode = this.address!!.postalCode, - city = this.address!!.city, - telephoneNumber = this.telephoneNumber, - mail = this.mail, - deliveryInformation = this.deliveryInformation - ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultFavouritePharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultFavouritePharmacyLocalDataSource.kt new file mode 100644 index 00000000..2aa806f0 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultFavouritePharmacyLocalDataSource.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository.datasource + +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.toInstant +import de.gematik.ti.erp.app.db.toRealmInstant +import de.gematik.ti.erp.app.db.tryWrite +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.query.Sort +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock + +class DefaultFavouritePharmacyLocalDataSource(private val realm: Realm) : FavouritePharmacyLocalDataSource { + override suspend fun deleteFavoritePharmacy(favoritePharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", favoritePharmacy.telematikId)?.let { delete(it) } + } + } + + override fun loadFavoritePharmacies(): Flow> { + return realm.query() + .sort("lastUsed", Sort.DESCENDING) + .asFlow() + .map { + it.list.map { favorite -> + favorite.toOverviewPharmacy() + } + } + } + + override suspend fun markPharmacyAsFavourite(pharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { + this.lastUsed = Clock.System.now().toRealmInstant() + } ?: copyToRealm(pharmacy.toFavoritePharmacyEntityV1()) + } + } + + override fun isPharmacyInFavorites(pharmacy: Pharmacy): Flow = + realm.query("telematikId = $0", pharmacy.telematikId) + .asFlow() + .map { + it.list.isNotEmpty() + } + + companion object { + private fun Pharmacy.toFavoritePharmacyEntityV1() = + FavoritePharmacyEntityV1().apply { + this.address = this@toFavoritePharmacyEntityV1.singleLineAddress() + this.pharmacyName = this@toFavoritePharmacyEntityV1.name + this.telematikId = this@toFavoritePharmacyEntityV1.telematikId + this.lastUsed = Clock.System.now().toRealmInstant() + } + + private fun FavoritePharmacyEntityV1.toOverviewPharmacy() = + OverviewPharmacy( + lastUsed = this.lastUsed.toInstant(), + telematikId = this.telematikId, + pharmacyName = this.pharmacyName, + address = this.address, + isFavorite = true, + usageCount = 0 + ) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultOftenUsePharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultOftenUsePharmacyLocalDataSource.kt new file mode 100644 index 00000000..ef7afdcc --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/DefaultOftenUsePharmacyLocalDataSource.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository.datasource + +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.OftenUsedPharmacyEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.toInstant +import de.gematik.ti.erp.app.db.toRealmInstant +import de.gematik.ti.erp.app.db.tryWrite +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.query.Sort +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock + +class DefaultOftenUsePharmacyLocalDataSource(private val realm: Realm) : OftenUsedPharmacyLocalDataSource { + + override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { + delete(it) + } + queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { + delete(it) + } + } + } + + override fun loadOftenUsedPharmacies(): Flow> = + realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { + it.list.map { pharmacy -> + pharmacy.toOverviewPharmacy() + } + } + + override suspend fun markPharmacyAsOftenUsed(pharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { + this.lastUsed = Clock.System.now().toRealmInstant() + this.usageCount += 1 + } ?: copyToRealm(pharmacy.toOftenUsedPharmacyEntityV1()) + } + } + + companion object { + fun Pharmacy.toOftenUsedPharmacyEntityV1() = + OftenUsedPharmacyEntityV1().apply { + this.address = this@toOftenUsedPharmacyEntityV1.singleLineAddress() + this.pharmacyName = this@toOftenUsedPharmacyEntityV1.name + this.telematikId = this@toOftenUsedPharmacyEntityV1.telematikId + this.lastUsed = Clock.System.now().toRealmInstant() + } + + fun OftenUsedPharmacyEntityV1.toOverviewPharmacy() = + OverviewPharmacy( + lastUsed = this.lastUsed.toInstant(), + usageCount = this.usageCount, + isFavorite = false, + telematikId = this.telematikId, + pharmacyName = this.pharmacyName, + address = this.address + ) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/FavouritePharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/FavouritePharmacyLocalDataSource.kt new file mode 100644 index 00000000..3a74bd14 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/FavouritePharmacyLocalDataSource.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository.datasource + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.flow.Flow + +interface FavouritePharmacyLocalDataSource { + fun loadFavoritePharmacies(): Flow> + suspend fun markPharmacyAsFavourite(pharmacy: PharmacyUseCaseData.Pharmacy) + suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) + fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/OftenUsedPharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/OftenUsedPharmacyLocalDataSource.kt new file mode 100644 index 00000000..50aefd47 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/OftenUsedPharmacyLocalDataSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pharmacy.repository.datasource + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.flow.Flow + +interface OftenUsedPharmacyLocalDataSource { + fun loadOftenUsedPharmacies(): Flow> + suspend fun markPharmacyAsOftenUsed(pharmacy: PharmacyUseCaseData.Pharmacy) + suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PharmacyRemoteDataSource.kt similarity index 75% rename from common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PharmacyRemoteDataSource.kt index 1ff617b1..05848fb7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/datasource/PharmacyRemoteDataSource.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.pharmacy.repository +package de.gematik.ti.erp.app.pharmacy.repository.datasource import de.gematik.ti.erp.app.api.PharmacyRedeemService import de.gematik.ti.erp.app.api.PharmacySearchService @@ -26,8 +26,8 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import java.net.URL -private const val PlaceholderTelematikId = "" -private const val PlaceholderTransactionId = "" +internal const val PlaceholderTelematikId = "" +internal const val PlaceholderTransactionId = "" class PharmacyRemoteDataSource( private val searchService: PharmacySearchService, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/model/CommunicationPayloadInbox.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/model/CommunicationPayloadInbox.kt index e586de02..35ac2d29 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/model/CommunicationPayloadInbox.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/model/CommunicationPayloadInbox.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.repository.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCase.kt index e3ec0e72..006ddf2a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase @@ -125,7 +125,7 @@ class GetShippingContactValidationUseCase { } @Requirement( - "O.Source_1#3", + "O.Source_1#4", sourceSpecification = "BSI-eRp-ePA", rationale = "analyse the user input of shipping contact data" ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt index 1801224c..e3b71e2f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase @@ -22,25 +22,16 @@ import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.pharmacy.buildDirectPharmacyMessage import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.util.encoders.Base64 +// TODO: Remove when the redemption in debug buttons is using the correct ones class PharmacyDirectRedeemUseCase( private val repository: PharmacyRepository ) { - suspend fun loadCertificates(locationId: String): Result> = - repository - .searchBinaryCerts(locationId = locationId).mapCatching { - list -> - list.map { base64Cert -> - X509CertificateHolder(Base64.decode(base64Cert)) - } - } - @Requirement( - "A_22778#1", - "A_22779#1", + "A_22778-01#2", + "A_22779-01#2", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Start Redeem without TI." + rationale = "Start Redeem without TI (useCase)." ) suspend fun redeemPrescriptionDirectly( url: String, @@ -62,8 +53,4 @@ class PharmacyDirectRedeemUseCase( transactionId = transactionId ).getOrThrow() } - - suspend fun markAsRedeemed(taskId: String) { - repository.markAsRedeemed(taskId) - } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt index 0c7e50c4..6af054d3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt @@ -1,97 +1,114 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase -import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.usecase.mapper.PharmacyInitialResultsPerPage import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext const val PharmacyMapNextResultsPerPage = 50 private const val PharmacyMapMaxResults = 120 +private const val DefaultRadiusInMeter = 999 * 1000.0 class PharmacyMapsUseCase( private val repository: PharmacyRepository, private val settingsRepository: SettingsRepository, - private val dispatchers: DispatchProvider + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - suspend fun searchPharmacies( - searchData: PharmacyUseCaseData.SearchData + @Suppress("MagicNumber") + suspend operator fun invoke( + searchData: PharmacyUseCaseData.MapsSearchData, + forcedRadius: Double? = null ): List = - withContext(dispatchers.io) { - settingsRepository.savePharmacySearch( - SettingsData.PharmacySearch( - name = searchData.name, - locationEnabled = searchData.locationMode !is PharmacyUseCaseData.LocationMode.Disabled, - deliveryService = searchData.filter.deliveryService, - onlineService = searchData.filter.onlineService, - openNow = searchData.filter.openNow + withContext(dispatcher) { + try { + settingsRepository.savePharmacySearch( + SettingsData.PharmacySearch( + name = searchData.name, + locationEnabled = searchData.locationMode !is PharmacyUseCaseData.LocationMode.Disabled, + deliveryService = searchData.filter.deliveryService, + onlineService = searchData.filter.onlineService, + openNow = searchData.filter.openNow + ) ) - ) - val names = searchData.name.split(" ").filter { it.isNotEmpty() } - val locationMode = searchData.locationMode - val filter = run { - val filterMap = mutableMapOf() - if (locationMode is PharmacyUseCaseData.LocationMode.Enabled) { - @Suppress("MagicNumber") - val radiusInKm = locationMode.radiusInMeter.toInt() / 1000 - val loc = locationMode.location - filterMap += "near" to "${loc.latitude}|${loc.longitude}|$radiusInKm|km" - } - if (searchData.filter.onlineService) { - filterMap += "type" to "mobl" + val names = searchData.name.split(" ").filter { it.isNotEmpty() } + val locationMode = searchData.locationMode + val filter = run { + val filterMap = mutableMapOf() + if (locationMode is PharmacyUseCaseData.LocationMode.Enabled) { + val radiusInKm = (forcedRadius?.toInt() ?: locationMode.radiusInMeter.toInt()) / 1000 + val loc = locationMode.coordinates + filterMap += "near" to "${loc.latitude}|${loc.longitude}|$radiusInKm|km" + } else if (locationMode is PharmacyUseCaseData.LocationMode.Disabled && + searchData.coordinates != null + ) { + val radiusInKm = (forcedRadius?.toInt() ?: DefaultRadiusInMeter.toInt()) / 1000 + val loc = searchData.coordinates + filterMap += "near" to "${loc.latitude}|${loc.longitude}|$radiusInKm|km" + } + if (searchData.filter.onlineService) { + filterMap += "type" to "mobl" + } + filterMap } - filterMap - } - val initialResult = repository.searchPharmacies( - names = names, - filter = filter - ).getOrThrow() + val initialResult = repository.searchPharmacies( + names = names, + filter = filter + ).getOrThrow() - if (initialResult.bundleResultCount == PharmacyInitialResultsPerPage) { - val pharmacies = initialResult.pharmacies.toModel().toMutableList() + if (initialResult.bundleResultCount == PharmacyInitialResultsPerPage) { + val pharmacies = initialResult.pharmacies.toModel().toMutableList() - var offset = initialResult.bundleResultCount - loop@ while (true) { - val result = repository.searchPharmaciesByBundle( - bundleId = initialResult.bundleId, - offset = offset, - count = PharmacyMapNextResultsPerPage - ).getOrThrow() + var offset = initialResult.bundleResultCount + loop@ while (true) { + val result = repository.searchPharmaciesByBundle( + bundleId = initialResult.bundleId, + offset = offset, + count = PharmacyMapNextResultsPerPage + ).getOrThrow() - if (result.bundleResultCount < PharmacyMapNextResultsPerPage || offset > PharmacyMapMaxResults) { - break@loop + if (result.bundleResultCount < PharmacyMapNextResultsPerPage || + offset > PharmacyMapMaxResults + ) { + break@loop + } + + pharmacies += result.pharmacies.toModel() + offset += result.bundleResultCount } - pharmacies += result.pharmacies.toModel() - offset += result.bundleResultCount + pharmacies + } else { + initialResult.pharmacies.toModel() } - - pharmacies - } else { - initialResult.pharmacies.toModel() + } catch (e: Throwable) { + Napier.e { "silent exception ${e.stackTraceToString()}" } + emptyList() } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt index c850e55c..1c338b6c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt @@ -1,27 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn @@ -38,7 +38,7 @@ class PharmacyOverviewUseCase( repository.loadFavoritePharmacies().flowOn(dispatchers.io) suspend fun saveOrUpdateUsedPharmacies(pharmacy: PharmacyUseCaseData.Pharmacy) { - repository.saveOrUpdateOftenUsedPharmacy(pharmacy) + repository.markPharmacyAsOftenUsed(pharmacy) } suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt index 54ad61d6..b357a56c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.pharmacy.usecase.mapper -import de.gematik.ti.erp.app.fhir.model.LocalPharmacyService import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.fhir.model.PharmacyService import de.gematik.ti.erp.app.pharmacy.model.PharmacyData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData @@ -34,14 +35,14 @@ fun List.toModel(): List = address = pharmacy.address.let { "${it.lines.joinToString()}\n${it.postalCode} ${it.city}" }, - location = pharmacy.location, + coordinates = pharmacy.coordinates, distance = null, contacts = pharmacy.contacts, provides = pharmacy.provides, openingHours = ( pharmacy.provides.find { - it is LocalPharmacyService - } as? LocalPharmacyService + it is PharmacyService.LocalPharmacyService + } as? PharmacyService.LocalPharmacyService )?.openingHours, telematikId = pharmacy.telematikId ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt index 9d9eb76a..62142abc 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt @@ -1,36 +1,41 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.pharmacy.usecase.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService -import de.gematik.ti.erp.app.fhir.model.Location -import de.gematik.ti.erp.app.fhir.model.OnlinePharmacyService +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.fhir.model.Coordinates import de.gematik.ti.erp.app.fhir.model.OpeningHours import de.gematik.ti.erp.app.fhir.model.PharmacyContacts import de.gematik.ti.erp.app.fhir.model.PharmacyService -import de.gematik.ti.erp.app.fhir.model.PickUpPharmacyService import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable private const val DefaultRadiusInMeter = 999 * 1000.0 object PharmacyUseCaseData { + @Requirement( + "A_20285#5", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = " .. filter pharmacies by different criteria." + ) @Immutable data class Filter( val nearBy: Boolean = false, @@ -46,12 +51,13 @@ object PharmacyUseCaseData { /** * Represents a pharmacy. */ + @Serializable @Immutable data class Pharmacy( val id: String, val name: String, val address: String?, - val location: Location?, + val coordinates: Coordinates?, val distance: Double?, val contacts: PharmacyContacts, val provides: List, @@ -59,13 +65,13 @@ object PharmacyUseCaseData { val telematikId: String ) { val isPickupService - get() = provides.any { it is PickUpPharmacyService } + get() = provides.any { it is PharmacyService.PickUpPharmacyService } val isDeliveryService - get() = provides.any { it is DeliveryPharmacyService } + get() = provides.any { it is PharmacyService.DeliveryPharmacyService } val isOnlineService - get() = provides.any { it is OnlinePharmacyService } + get() = provides.any { it is PharmacyService.OnlinePharmacyService } val directRedeemUrlsNotPresent: Boolean get() { @@ -94,12 +100,23 @@ object PharmacyUseCaseData { data object Disabled : LocationMode() @Immutable - data class Enabled(val location: Location, val radiusInMeter: Double = DefaultRadiusInMeter) : LocationMode() + data class Enabled( + val coordinates: Coordinates, + val radiusInMeter: Double = DefaultRadiusInMeter + ) : LocationMode() } @Immutable data class SearchData(val name: String, val filter: Filter, val locationMode: LocationMode) + @Immutable + data class MapsSearchData( + val name: String, + val filter: Filter, + val locationMode: LocationMode, + val coordinates: Coordinates? + ) + /** * State with list of pharmacies */ @@ -113,9 +130,11 @@ object PharmacyUseCaseData { val taskId: String, val accessCode: String, val title: String?, + val isSelfPayerPrescription: Boolean, val index: Int?, val timestamp: Instant, - val substitutionsAllowed: Boolean + val substitutionsAllowed: Boolean, + val isScanned: Boolean ) @Immutable @@ -163,12 +182,14 @@ object PharmacyUseCaseData { @Immutable data class OrderState( - val orders: List, + val prescriptionOrders: List, + val selfPayerPrescriptionIds: List, val contact: ShippingContact ) { companion object { val Empty = OrderState( - orders = emptyList(), + prescriptionOrders = emptyList(), + selfPayerPrescriptionIds = emptyList(), contact = ShippingContact.EmptyShippingContact ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt index 5f3973ce..07b8af52 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt index 37856c05..245232fb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt @@ -1,26 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.model import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +@Serializable data class Communication( val taskId: String, val communicationId: String, @@ -34,7 +36,7 @@ data class Communication( ) enum class CommunicationProfile { - ErxCommunicationDispReq, ErxCommunicationReply; + ErxCommunicationDispReq, ErxCommunicationReply, InApp; fun toEntityValue() = when (this) { ErxCommunicationDispReq -> @@ -42,5 +44,7 @@ enum class CommunicationProfile { ErxCommunicationReply -> CommunicationProfileV1.ErxCommunicationReply + + InApp -> InApp }.name } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionData.kt similarity index 75% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionData.kt index d02bebdf..88305848 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionData.kt @@ -1,27 +1,27 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -package de.gematik.ti.erp.app.prescription.detail.ui.model +package de.gematik.ti.erp.app.prescription.model +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.prescription.model.ScannedTaskData -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName @@ -31,17 +31,16 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject object PrescriptionData { - var scannedPrescriptionIndex: Int = 0 - + @Immutable sealed interface Prescription { val profileId: ProfileIdentifier val taskId: String val redeemedOn: Instant? - val accessCode: String? + val accessCode: String } @Stable - class Scanned( + data class Scanned( val task: ScannedTaskData.ScannedTask ) : Prescription { override val profileId: ProfileIdentifier = task.profileId @@ -50,19 +49,20 @@ object PrescriptionData { override val accessCode: String = task.accessCode val scannedOn: Instant = task.scannedOn val index: Int = task.index - val name: String? = task.name + val name: String = task.name val isRedeemed = redeemedOn != null } @Stable - class Synced( - val task: SyncedTaskData.SyncedTask + data class Synced( + val task: SyncedTaskData.SyncedTask, + val now: Instant = Clock.System.now() ) : Prescription { override val profileId: ProfileIdentifier = task.profileId override val taskId: String = task.taskId override val redeemedOn: Instant? = task.redeemedOn() - override val accessCode: String? = task.accessCode - + override val accessCode: String = task.accessCode + val redeemState = task.redeemState(now) val name = task.medicationName() val state: SyncedTaskData.SyncedTask.TaskState = task.state() val authoredOn: Instant = task.authoredOn diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionDetailBottomSheetNavigationData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionDetailBottomSheetNavigationData.kt new file mode 100644 index 00000000..9554235a --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionDetailBottomSheetNavigationData.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.detail.ui.model + +data class PrescriptionDetailBottomSheetNavigationData( + val selPayerPrescriptionBottomSheet: () -> Unit = {}, + val additionalFeeNotExemptBottomSheet: () -> Unit = {}, + val additionalFeeExemptBottomSheet: () -> Unit = {}, + val failureBottomSheet: () -> Unit = {}, + val directAssignmentBottomSheet: () -> Unit = {}, + val substitutionAllowedBottomSheet: () -> Unit = {}, + val substitutionNotAllowedBottomSheet: () -> Unit = {}, + val emergencyFeeNotExemptBottomSheet: () -> Unit = {}, + val emergencyFeeExemptBottomSheet: () -> Unit = {}, + val scannedPrescriptionBottomSheet: () -> Unit = {} +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionType.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionType.kt new file mode 100644 index 00000000..3cef4499 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/PrescriptionType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.model + +enum class PrescriptionType { + ScannedTask, SyncedTask +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Ratio.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Ratio.kt new file mode 100644 index 00000000..2200b9ef --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Ratio.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.prescription.model + +import de.gematik.ti.erp.app.db.entities.v1.task.QuantityEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 +import kotlinx.serialization.Serializable + +@Serializable +data class Quantity( + val value: String, + val unit: String +) + +@Serializable +data class Ratio( + val numerator: Quantity?, + val denominator: Quantity? +) { + fun toRatioEntity(): RatioEntityV1 = RatioEntityV1().apply { + this.numerator = QuantityEntityV1().apply { + this.value = this@Ratio.numerator?.value ?: "" + this.unit = this@Ratio.numerator?.unit ?: "" + } + this.denominator = QuantityEntityV1().apply { + this.value = this@Ratio.numerator?.value ?: "" + this.unit = this@Ratio.numerator?.unit ?: "" + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt index c09295bd..b6c89fb4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.model @@ -26,7 +26,7 @@ object ScannedTaskData { val profileId: ProfileIdentifier, val taskId: String, val index: Int, - val name: String?, + val name: String, val accessCode: String, val scannedOn: Instant, val redeemedOn: Instant?, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt index 86e13317..455a677d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.model @@ -22,6 +22,8 @@ import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.toStartOfDayInUTC import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.daysUntil import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -31,7 +33,6 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlin.time.Duration -import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.minutes val CommunicationWaitStateDelta: Duration = 10.minutes @@ -45,10 +46,42 @@ object SyncedTaskData { Ready, InProgress, Completed, Other, Draft, Requested, Received, Accepted, Rejected, Canceled, OnHold, Failed } + enum class TaskStateSerializationType { + Ready, + Deleted, + LaterRedeemable, + Pending, + InProgress, + Expired, + Provided, + Other + } + + enum class CoverageType { + GKV, // Gesetzliche Krankenversicherung + PKV, // Private Krankenversicherung + BG, // Berufsgenossenschaft + SEL, // Selbstzahler + SOZ, // Sozialamt + GPV, // Gesetzliche Pflegeversicherung + PPV, // Private Pflegeversicherung + BEI, // Beihilfe + UNKNOWN; + + companion object { + fun mapTo(value: String?): CoverageType = + try { + valueOf(value ?: UNKNOWN.toString()) + } catch (e: Throwable) { + UNKNOWN + } + } + } + data class SyncedTask( val profileId: String, val taskId: String, - val accessCode: String?, + val accessCode: String, val lastModified: Instant, val organization: Organization, val practitioner: Practitioner, @@ -62,36 +95,119 @@ object SyncedTaskData { var pvsIdentifier: String, var failureToReport: String, val medicationRequest: MedicationRequest, + val currentTime: Instant = Clock.System.now(), // TODO: figure out a way to remove this and make previews work val medicationDispenses: List = emptyList(), + val lastMedicationDispense: Instant?, val communications: List = emptyList() ) { - sealed interface TaskState - - data class Ready(val expiresOn: Instant, val acceptUntil: Instant) : TaskState - data class LaterRedeemable(val redeemableOn: Instant) : TaskState + @Serializable(with = TaskStateSyncedTaskDataSerializer::class) + sealed interface TaskState { + val type: TaskStateSerializationType + } - data class Pending(val sentOn: Instant, val toTelematikId: String) : TaskState - data class InProgress(val lastModified: Instant) : TaskState - data class Expired(val expiredOn: Instant) : TaskState + @Serializable + @SerialName("Ready") + data class Ready( + override val type: TaskStateSerializationType = TaskStateSerializationType.Ready, + val expiresOn: Instant, + val acceptUntil: Instant + ) : TaskState { + fun acceptDaysLeft(now: Instant): Int = + now.daysUntil(acceptUntil, TimeZone.currentSystemDefault()) + + // -1 because on the day of expiresOn, the the prescription is not redeemable + fun expiryDaysLeft(now: Instant): Int = + now.daysUntil(expiresOn, TimeZone.currentSystemDefault()) - 1 + } - data class Other(val state: TaskStatus, val lastModified: Instant) : TaskState + @Serializable + @SerialName("Deleted") + data class Deleted( + override val type: TaskStateSerializationType = TaskStateSerializationType.Deleted, + val lastModified: Instant + ) : TaskState + + @Serializable + @SerialName("LaterRedeemable") + data class LaterRedeemable( + override val type: TaskStateSerializationType = TaskStateSerializationType.LaterRedeemable, + val redeemableOn: Instant + ) : TaskState + + @Serializable + @SerialName("Pending") + data class Pending( + override val type: TaskStateSerializationType = TaskStateSerializationType.Pending, + val sentOn: Instant, + val toTelematikId: String + ) : TaskState + + @Serializable + @SerialName("InProgress") + data class InProgress( + override val type: TaskStateSerializationType = TaskStateSerializationType.InProgress, + val lastModified: Instant + ) : TaskState + + @Serializable + @SerialName("Expired") + data class Expired( + override val type: TaskStateSerializationType = TaskStateSerializationType.Expired, + val expiredOn: Instant + ) : TaskState + + @Serializable + @SerialName("InProgress") + data class Provided( + override val type: TaskStateSerializationType = TaskStateSerializationType.Provided, + val lastMedicationDispense: Instant + ) : TaskState + + @Serializable + @SerialName("Other") + data class Other( + override val type: TaskStateSerializationType = TaskStateSerializationType.Other, + val state: TaskStatus, + val lastModified: Instant + ) : TaskState + + object TaskStateSyncedTaskDataSerializer : JsonContentPolymorphicSerializer( + TaskState::class + ) { + override fun selectDeserializer(element: JsonElement): KSerializer { + element.jsonObject["type"]?.jsonPrimitive?.content?.let { classType -> + return when (TaskStateSerializationType.valueOf(classType)) { + TaskStateSerializationType.Ready -> Ready.serializer() + TaskStateSerializationType.Deleted -> Deleted.serializer() + TaskStateSerializationType.LaterRedeemable -> LaterRedeemable.serializer() + TaskStateSerializationType.Pending -> Pending.serializer() + TaskStateSerializationType.InProgress -> InProgress.serializer() + TaskStateSerializationType.Expired -> Expired.serializer() + TaskStateSerializationType.Provided -> Provided.serializer() + TaskStateSerializationType.Other -> Other.serializer() + } + } ?: throw SerializationException( + "TaskStateSyncedTaskDataSerializer: key 'type' not found or does not match any task state type" + ) + } + } - fun state(now: Instant = Clock.System.now(), delta: Duration = CommunicationWaitStateDelta): TaskState = + fun state(now: Instant = currentTime, delta: Duration = CommunicationWaitStateDelta): TaskState = when { medicationRequest.multiplePrescriptionInfo.indicator && medicationRequest.multiplePrescriptionInfo.start?.let { start -> start > now } == true -> { - LaterRedeemable(medicationRequest.multiplePrescriptionInfo.start) + LaterRedeemable(redeemableOn = medicationRequest.multiplePrescriptionInfo.start) } // expiration date is issue day + 3 months until 0:00 AM on that day expiresOn != null && expiresOn <= now.toStartOfDayInUTC() && status != TaskStatus.Completed -> - Expired(expiresOn) + Expired(expiredOn = expiresOn) - status == TaskStatus.Ready && accessCode != null && - communications.any { it.profile == CommunicationProfile.ErxCommunicationDispReq } && - redeemState(now, delta) == RedeemState.NotRedeemable -> { + status == TaskStatus.Ready && communications.any { + it.profile == CommunicationProfile.ErxCommunicationDispReq + } && redeemState(now, delta) == RedeemState.RedeemableAfterDelta -> { val comm = this.communications .filter { it.profile == CommunicationProfile.ErxCommunicationDispReq } .maxBy { it.sentOn } @@ -103,16 +219,17 @@ object SyncedTaskData { } status == TaskStatus.Ready -> Ready( - // Expires on "expiresOn"-day at 0:00 AM. - // Minus 1 day to use it as the last possible day of redeemability - expiresOn = requireNotNull(expiresOn?.minus(1.days)), - // Not Redeemable at the cost of the healthinsurancecompany (HI) on this day at 0:00 AM - // Minus 1 day to use it as the last possible day of redeemability at the cost of the HI - acceptUntil = requireNotNull(acceptUntil?.minus(1.days)) + expiresOn = requireNotNull(expiresOn) { "expiresOn is wrong" }, + acceptUntil = requireNotNull(acceptUntil) { "acceptUntil is wrong" } + ) + + status == TaskStatus.Canceled -> Deleted(lastModified = this.lastModified) + status != TaskStatus.Completed && lastMedicationDispense != null -> Provided( + lastMedicationDispense = this.lastMedicationDispense ) status == TaskStatus.InProgress -> InProgress(lastModified = this.lastModified) - else -> Other(this.status, this.lastModified) + else -> Other(state = status, lastModified = this.lastModified) } enum class RedeemState { @@ -120,12 +237,12 @@ object SyncedTaskData { RedeemableAndValid, RedeemableAfterDelta; - fun isRedeemable() = this != NotRedeemable + fun isRedeemable() = this == RedeemableAndValid } fun redeemedOn() = if (status == TaskStatus.Completed) { - medicationDispenses.firstOrNull()?.whenHandedOver?.toInstant() ?: lastModified + lastModified } else { null } @@ -134,6 +251,7 @@ object SyncedTaskData { * The list of redeemable prescriptions. Should NOT be used as a filter for the active/archive tab! * See [isActive] for a decision it this prescription should be shown in the "Active" or "Archive" tab. */ + @Suppress("CyclomaticComplexMethod") fun redeemState(now: Instant = Clock.System.now(), delta: Duration = CommunicationWaitStateDelta): RedeemState { val expired = (expiresOn != null && expiresOn <= now.toStartOfDayInUTC()) val redeemableLater = medicationRequest.multiplePrescriptionInfo.indicator && @@ -141,27 +259,27 @@ object SyncedTaskData { it > now } == true val ready = status == TaskStatus.Ready - val valid = accessCode != null + val inProgress = status == TaskStatus.InProgress val latestDispenseReqCommunication = communications .filter { it.profile == CommunicationProfile.ErxCommunicationDispReq } .maxOfOrNull { it.sentOn } - // if lastModified is more recent than the latest disp req, we can be sure that something - // happened with the task (e.g. claimed -> rejected) - val isDeltaLocked = latestDispenseReqCommunication?.let { lastModified < it && (it + delta) > now } + + val isDeltaLocked = latestDispenseReqCommunication?.let { lastModified < it && (it + delta) > now } ?: false + val valid = accessCode.isNotEmpty() return when { - redeemableLater || expired -> RedeemState.NotRedeemable - ready && valid && latestDispenseReqCommunication == null -> RedeemState.RedeemableAndValid - ready && valid && isDeltaLocked == false -> RedeemState.RedeemableAfterDelta - ready && valid && isDeltaLocked == true -> RedeemState.NotRedeemable + redeemableLater || expired || inProgress -> RedeemState.NotRedeemable + ready && valid && isDeltaLocked -> RedeemState.RedeemableAfterDelta + ready && valid && !isDeltaLocked -> RedeemState.RedeemableAndValid else -> RedeemState.NotRedeemable } } fun isActive(now: Instant = Clock.System.now()): Boolean { val expired = expiresOn != null && expiresOn <= now.toStartOfDayInUTC() - val allowedStatus = status == TaskStatus.Ready || status == TaskStatus.InProgress - return !expired && allowedStatus + val wasActiveAndThenCanceled = !expired && medicationDispenses.isEmpty() && status == TaskStatus.Canceled + val allowedStatus = status in setOf(TaskStatus.Ready, TaskStatus.InProgress) + return (!expired && allowedStatus) || wasActiveAndThenCanceled } fun isDirectAssignment() = @@ -230,7 +348,8 @@ object SyncedTaskData { @Serializable data class InsuranceInformation( val name: String? = null, - val status: String? = null + val status: String? = null, + val coverageType: CoverageType ) @Serializable @@ -303,18 +422,6 @@ object SyncedTaskData { UNKNOWN } - @Serializable - data class Quantity( - val value: String, - val unit: String - ) - - @Serializable - data class Ratio( - val numerator: Quantity?, - val denominator: Quantity? - ) - @Serializable data class Ingredient( var text: String, @@ -324,117 +431,38 @@ object SyncedTaskData { var strength: Ratio? ) - /* - Since kotlinx.serialization does not support PolymorphicSerializer of nullable types - out of the box we need to add a type to let the serializer know the difference if it is - a sealed class or sealed interface. - */ - enum class MedicationSerializationType { - MedicationFreeText, - MedicationIngredient, - MedicationCompounding, - MedicationPZN - } - - @Serializable(with = MedicationSyncedTaskDataSerializer::class) - sealed interface Medication { - val type: MedicationSerializationType - fun name(): String - - val category: MedicationCategory - val vaccine: Boolean - val text: String - val form: String? - val lotNumber: String? - val expirationDate: FhirTemporal? - } - @Serializable - @SerialName("MedicationFreeText") - data class MedicationFreeText( - override val type: MedicationSerializationType = MedicationSerializationType.MedicationFreeText, - override val category: MedicationCategory, - override val vaccine: Boolean, - override val text: String, - override val form: String?, - override val lotNumber: String?, - override val expirationDate: FhirTemporal? - ) : Medication { - override fun name(): String = text - } + data class Identifier( + var pzn: String? = null, + var atc: String? = null, + var ask: String? = null, + var snomed: String? = null + ) @Serializable - @SerialName("MedicationIngredient") - data class MedicationIngredient( - override val type: MedicationSerializationType = MedicationSerializationType.MedicationIngredient, - override val category: MedicationCategory, - override val vaccine: Boolean, - override val text: String, - override val form: String?, - override val lotNumber: String?, - override val expirationDate: FhirTemporal?, + @SerialName("Medication") + data class Medication( + val category: MedicationCategory, + val vaccine: Boolean, + val text: String, + val form: String?, + val lotNumber: String?, + val expirationDate: FhirTemporal?, + val identifier: Identifier, val normSizeCode: String?, val amount: Ratio?, - val ingredients: List - - ) : Medication { - override fun name(): String = joinIngredientNames(ingredients) - } - - @Serializable - @SerialName("MedicationCompounding") - data class MedicationCompounding( - override val type: MedicationSerializationType = MedicationSerializationType.MedicationCompounding, - override val category: MedicationCategory, - override val vaccine: Boolean, - override val text: String, - override val form: String?, - override val lotNumber: String?, - override val expirationDate: FhirTemporal?, val manufacturingInstructions: String?, val packaging: String?, - val amount: Ratio?, + val ingredientMedications: List, val ingredients: List - - ) : Medication { - override fun name(): String = joinIngredientNames(ingredients) - } - - @Serializable - @SerialName("MedicationPZN") - data class MedicationPZN( - override val type: MedicationSerializationType = MedicationSerializationType.MedicationPZN, - override val category: MedicationCategory, - override val vaccine: Boolean, - override val text: String, - override val form: String?, - override val lotNumber: String?, - override val expirationDate: FhirTemporal?, - val uniqueIdentifier: String, - val normSizeCode: String?, - val amount: Ratio? - - ) : Medication { - override fun name() = text + ) { + fun name() = text.ifEmpty { + joinIngredientNames(ingredients) + } } fun joinIngredientNames(ingredients: List) = ingredients.joinToString(", ") { ingredient -> ingredient.text } - - object MedicationSyncedTaskDataSerializer : JsonContentPolymorphicSerializer(Medication::class) { - override fun selectDeserializer(element: JsonElement): KSerializer { - element.jsonObject["type"]?.jsonPrimitive?.content?.let { classType -> - return when (MedicationSerializationType.valueOf(classType)) { - MedicationSerializationType.MedicationFreeText -> MedicationFreeText.serializer() - MedicationSerializationType.MedicationIngredient -> MedicationIngredient.serializer() - MedicationSerializationType.MedicationCompounding -> MedicationCompounding.serializer() - MedicationSerializationType.MedicationPZN -> MedicationPZN.serializer() - } - } ?: throw SerializationException( - "MedicationSyncedTaskDataSerializer: key 'type' not found or does not matches any module type" - ) - } - } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt index f00fd43a..cd74eeb6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -23,7 +23,6 @@ import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import kotlinx.datetime.Instant @@ -44,9 +43,13 @@ class DefaultPrescriptionRepository( /** * Saves all scanned tasks. It doesn't matter if they already exist. */ - override suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) { + override suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List, + medicationString: String + ) { withContext(dispatchers.io) { - localDataSource.saveScannedTasks(profileId, tasks) + localDataSource.saveScannedTasks(profileId, tasks, medicationString) } } @@ -59,25 +62,16 @@ class DefaultPrescriptionRepository( override suspend fun redeemPrescription( profileId: ProfileIdentifier, communication: JsonElement, - accessCode: String? - ): Result = withContext(dispatchers.io) { - remoteDataSource.communicate(profileId, communication, accessCode).map { } + accessCode: String + ): Result = withContext(dispatchers.io) { + remoteDataSource.communicate(profileId, communication, accessCode) } - override suspend fun deleteTaskByTaskId( + override suspend fun deleteRemoteTaskById( profileId: ProfileIdentifier, taskId: String - ): Result = withContext(dispatchers.io) { - // check if task is local only - if (localDataSource.loadScannedTaskByTaskId(taskId).first() != null) { - localDataSource.deleteTask(taskId) - Result.success(Unit) - } else { - remoteDataSource.deleteTask(profileId, taskId).map { - localDataSource.deleteTask(taskId) - } - } - } + ): Result = + remoteDataSource.deleteTask(profileId, taskId).map { it } override suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) = localDataSource.updateRedeemedOn(taskId, timestamp) @@ -92,4 +86,21 @@ class DefaultPrescriptionRepository( localDataSource.loadScannedTaskByTaskId(taskId) override fun loadTaskIds(): Flow> = localDataSource.loadTaskIds().flowOn(dispatchers.io) + + override suspend fun deleteLocalTaskById(taskId: String) { + localDataSource.deleteTask(taskId) + } + override suspend fun deleteLocalInvoicesById(taskId: String) { + localDataSource.deleteInvoices(taskId) + } + + override suspend fun wasProfileEverAuthenticated(profileId: ProfileIdentifier): Boolean = + localDataSource.wasProfileEverAuthenticated(profileId) + + override suspend fun redeemScannedTasks(taskIds: List) { + localDataSource.redeemScannedTasks(taskIds) + } + + override fun loadAllTaskIds(profileId: ProfileIdentifier): Flow> = + localDataSource.loadAllTaskIds(profileId) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt index 7a1715b0..78fb9feb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt @@ -1,26 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.api.ResourcePaging -import de.gematik.ti.erp.app.fhir.model.extractTaskIds +import de.gematik.ti.erp.app.fhir.model.TaskStatus +import de.gematik.ti.erp.app.fhir.model.extractActualTaskData +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.async @@ -58,28 +61,34 @@ class DefaultTaskRepository( count = count ).mapCatching { fhirBundle -> withContext(dispatchers.io) { - val (total, taskIds) = extractTaskIds(fhirBundle) + val (total, taskData) = extractActualTaskData(fhirBundle) supervisorScope { - val results = taskIds.map { taskId -> + val results = taskData.map { data -> async { - downloadTaskWithKBVBundle(taskId = taskId, profileId = profileId).map { - if (it.isCompleted) { - downloadMedicationDispenses( - profileId, - taskId - ) + data.lastModified?.let { + if (data.status == TaskStatus.Canceled) { + localDataSource.updateTaskStatus(data.taskId, data.status, data.lastModified) + Result.success(Unit) + } else { + downloadTaskWithKBVBundle(taskId = data.taskId, profileId = profileId).map { + if (it.isCompleted || it.lastMedicationDispense != null) { + // download medication dispenses only if task was successfully + // downloaded and status is completed or last medication dispense + downloadMedicationDispenses( + profileId, + data.taskId + ) + } + } } - - requireNotNull(it.lastModified) } } }.awaitAll() - // throw if any result is not parsed correctly - results.find { it.isFailure }?.getOrThrow() + results.find { it?.isFailure ?: false }?.getOrThrow() - // return number of bundles saved to db + // return number of updated tasks ResourceResult(total, results.size) } } @@ -99,10 +108,18 @@ class DefaultTaskRepository( taskId: String ): Result = withContext(dispatchers.io) { remoteDataSource.loadBundleOfMedicationDispenses(profileId, taskId).map { bundle -> - bundle.findAll("entry.resource") - .forEach { dispense -> + val resources = bundle.findAll("entry.resource").toList() + val version = resources.first().contained("meta") + .contained("profile") + .containedString().split("|")[1] + when (version) { + "1.4" -> { + localDataSource.saveMedicationDispensesWithMedications(taskId, bundle) + } + else -> resources.forEach { dispense -> localDataSource.saveMedicationDispense(taskId, dispense) } + } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt index 016aab59..95fd5d34 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.prescription.repository import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.db.entities.deleteAll import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 @@ -33,6 +34,7 @@ import de.gematik.ti.erp.app.fhir.model.extractCommunications import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.utils.toStartOfDayInUTC import io.realm.kotlin.Realm import io.realm.kotlin.ext.query import io.realm.kotlin.query.Sort @@ -46,10 +48,20 @@ import kotlinx.serialization.json.JsonElement class PrescriptionLocalDataSource( private val realm: Realm ) { - suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) { + suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List, + medicationString: String + ) { realm.tryWrite { queryFirst("id = $0", profileId)?.let { profile -> - tasks.forEach { task -> + val todaysNumberOfTasks = query( + "parent = $0 AND scannedOn >= $1", + profile, + Clock.System.now().toStartOfDayInUTC().toRealmInstant() + ).count().find().toInt() + + tasks.forEachIndexed { idx, task -> if (query( "syncedTasks.taskId = $0 OR scannedTasks.taskId = $0", task.taskId @@ -58,11 +70,11 @@ class PrescriptionLocalDataSource( profile.scannedTasks += copyToRealm( ScannedTaskEntityV1().apply { this.index = task.index + 1 - this.name = task.name + this.name = "$medicationString ${todaysNumberOfTasks + idx + 1}" this.parent = profile this.taskId = task.taskId this.accessCode = task.accessCode - this.scannedOn = task.scannedOn.toRealmInstant() + this.scannedOn = Clock.System.now().toRealmInstant() this.redeemedOn = task.redeemedOn?.toRealmInstant() } ) @@ -115,7 +127,7 @@ class PrescriptionLocalDataSource( this.communicationId = communicationId this.orderId = orderId ?: "" this.sentOn = sentOn.value.toRealmInstant() - this.sender = sender + this.sender = sender ?: "" this.recipient = recipient this.payload = payload this.consumed = false @@ -130,6 +142,7 @@ class PrescriptionLocalDataSource( totalCommunicationsInBundle } + // ToDo: The Pharmacy-ID should be saved as recipient, and this should then be migrated correctly suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { realm.tryWrite { val entity = CommunicationEntityV1().apply { @@ -144,7 +157,6 @@ class PrescriptionLocalDataSource( queryFirst("taskId = $0", taskId)?.let { scannedTask -> scannedTask.communications += copyToRealm(entity) } - 1 } } @@ -160,16 +172,14 @@ class PrescriptionLocalDataSource( } @Requirement( - "A_19229#2", + "A_19229-01#2", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "User can a locally stored prescription and all its linked resources." + rationale = "User can delete a locally stored prescription and all its linked resources." ) suspend fun deleteTask(taskId: String) { realm.tryWrite { - queryFirst("taskId = $0", taskId)?.let { delete(it) } - queryFirst("taskId = $0", taskId)?.let { - deleteAll(it) - } + queryFirst("taskId = $0", taskId)?.let { deleteAll(it) } + queryFirst("taskId = $0", taskId)?.let { deleteAll(it) } } } @@ -196,4 +206,33 @@ class PrescriptionLocalDataSource( ) { syncedTasks, scannedTasks -> syncedTasks.list.map { it.taskId } + scannedTasks.list.map { it.taskId } } + + fun loadAllTaskIds(profileId: ProfileIdentifier): Flow> = + combine( + realm.query("parent.id = $0", profileId).asFlow(), + realm.query("parent.id = $0", profileId).asFlow() + ) { syncedTasks, scannedTasks -> + syncedTasks.list.map { it.taskId } + scannedTasks.list.map { it.taskId } + } + + fun wasProfileEverAuthenticated(profileId: ProfileIdentifier): Boolean = + realm.queryFirst("id = $0", profileId)?.let { profile -> + profile.lastAuthenticated != null + } ?: false + + suspend fun redeemScannedTasks(taskIds: List) { + realm.tryWrite { + taskIds.forEach { taskId -> + queryFirst("taskId = $0", taskId)?.let { + it.redeemedOn = Clock.System.now().toRealmInstant() + } + } + } + } + + suspend fun deleteInvoices(taskId: String) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.let { deleteAll(it) } + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt index f6c52e1b..8bc9ffd7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -41,13 +41,11 @@ class PrescriptionRemoteDataSource( ) } - suspend fun deleteTask(profileId: ProfileIdentifier, taskId: String) = safeApiCallNullable( - "error deleting task $taskId" - ) { + suspend fun deleteTask(profileId: ProfileIdentifier, taskId: String) = safeApiCallNullable { service.deleteTask(profileId, id = taskId) } - suspend fun communicate(profileId: ProfileIdentifier, communication: JsonElement, accessCode: String? = null) = + suspend fun communicate(profileId: ProfileIdentifier, communication: JsonElement, accessCode: String) = safeApiCall(errorMessage = "error while posting communication") { service.postCommunication(profileId = profileId, communication = communication, accessCode = accessCode) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt index 6f8a5e1b..5c104ffb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -26,7 +26,11 @@ import kotlinx.datetime.Instant import kotlinx.serialization.json.JsonElement interface PrescriptionRepository { - suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) + suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List, + medicationString: String + ) fun scannedTasks(profileId: ProfileIdentifier): Flow> @@ -35,13 +39,13 @@ interface PrescriptionRepository { suspend fun redeemPrescription( profileId: ProfileIdentifier, communication: JsonElement, - accessCode: String? = null - ): Result + accessCode: String + ): Result - suspend fun deleteTaskByTaskId( + suspend fun deleteRemoteTaskById( profileId: ProfileIdentifier, taskId: String - ): Result + ): Result suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) @@ -52,4 +56,10 @@ interface PrescriptionRepository { fun loadScannedTaskByTaskId(taskId: String): Flow fun loadTaskIds(): Flow> + suspend fun deleteLocalTaskById(taskId: String) + suspend fun wasProfileEverAuthenticated(profileId: ProfileIdentifier): Boolean + suspend fun redeemScannedTasks(taskIds: List) + + fun loadAllTaskIds(profileId: ProfileIdentifier): Flow> + suspend fun deleteLocalInvoicesById(taskId: String) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt index c8e057ef..695ef6e2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository @@ -23,12 +23,13 @@ import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.AccidentTypeV1 import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 +import de.gematik.ti.erp.app.db.entities.v1.task.CoverageTypeV1 +import de.gematik.ti.erp.app.db.entities.v1.task.IdentifierEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.IngredientEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.InsuranceInformationEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationCategoryV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationDispenseEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MedicationProfileV1 import de.gematik.ti.erp.app.db.entities.v1.task.MedicationRequestEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.MultiplePrescriptionInfoEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.OrganizationEntityV1 @@ -43,16 +44,20 @@ import de.gematik.ti.erp.app.db.queryFirst import de.gematik.ti.erp.app.db.toInstant import de.gematik.ti.erp.app.db.toRealmInstant import de.gematik.ti.erp.app.db.tryWrite +import de.gematik.ti.erp.app.db.writeToRealm import de.gematik.ti.erp.app.fhir.model.AccidentType import de.gematik.ti.erp.app.fhir.model.MedicationCategory -import de.gematik.ti.erp.app.fhir.model.MedicationProfile import de.gematik.ti.erp.app.fhir.model.TaskStatus import de.gematik.ti.erp.app.fhir.model.extractKBVBundle import de.gematik.ti.erp.app.fhir.model.extractMedicationDispense +import de.gematik.ti.erp.app.fhir.model.extractMedicationDispensePairs +import de.gematik.ti.erp.app.fhir.model.extractMedicationDispenseWithMedication import de.gematik.ti.erp.app.fhir.model.extractTask import de.gematik.ti.erp.app.fhir.model.extractTaskAndKBVBundle import de.gematik.ti.erp.app.prescription.model.Communication import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -77,7 +82,8 @@ class TaskLocalDataSource( ) { data class SaveTaskResult( val isCompleted: Boolean, - val lastModified: Instant + val lastModified: Instant, + val lastMedicationDispense: Instant? = null ) fun latestTaskModifiedTimestamp(profileId: ProfileIdentifier): Flow = @@ -88,6 +94,17 @@ class TaskLocalDataSource( it?.toInstant() } + suspend fun updateTaskStatus(taskId: String, status: TaskStatus, lastModified: FhirTemporal?) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.let { task -> + lastModified?.let { + task.lastModified = lastModified.toInstant().toRealmInstant() + task.status = status.toTaskStatusV1() + } + } + } + } + private val mutex = Mutex() @Suppress("LongMethod", "ComplexMethod") @@ -101,9 +118,10 @@ class TaskLocalDataSource( process = { taskResource, bundleResource -> extractTask( task = taskResource, - process = { taskId: String, accessCode: String?, lastModified: FhirTemporal.Instant, + process = { taskId: String, accessCode: String, lastModified: FhirTemporal.Instant, expiresOn: FhirTemporal.LocalDate?, acceptUntil: FhirTemporal.LocalDate?, - authoredOn: FhirTemporal.Instant, status: TaskStatus -> + authoredOn: FhirTemporal.Instant, status: TaskStatus, + lastMedicationDispense: FhirTemporal.Instant? -> taskEntity = queryFirst("taskId = $0", taskId) ?: run { copyToRealm(SyncedTaskEntityV1()).also { @@ -116,22 +134,10 @@ class TaskLocalDataSource( this.taskId = taskId this.accessCode = accessCode this.lastModified = lastModified.value.toRealmInstant() - this.status = when (status) { - TaskStatus.Ready -> TaskStatusV1.Ready - TaskStatus.InProgress -> TaskStatusV1.InProgress - TaskStatus.Completed -> TaskStatusV1.Completed - TaskStatus.Draft -> TaskStatusV1.Draft - TaskStatus.Requested -> TaskStatusV1.Requested - TaskStatus.Received -> TaskStatusV1.Received - TaskStatus.Accepted -> TaskStatusV1.Accepted - TaskStatus.Rejected -> TaskStatusV1.Rejected - TaskStatus.Canceled -> TaskStatusV1.Canceled - TaskStatus.OnHold -> TaskStatusV1.OnHold - TaskStatus.Failed -> TaskStatusV1.Failed - else -> TaskStatusV1.Other - } + this.status = status.toTaskStatusV1() this.expiresOn = expiresOn?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() + this.lastMedicationDispense = lastMedicationDispense?.toInstant()?.toRealmInstant() this.acceptUntil = acceptUntil?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() this.authoredOn = authoredOn.value.toRealmInstant() @@ -166,10 +172,11 @@ class TaskLocalDataSource( this.practitionerIdentifier = practitionerIdentifier } }, - processInsuranceInformation = { name, statusCode -> + processInsuranceInformation = { name, statusCode, coverageType -> InsuranceInformationEntityV1().apply { this.name = name this.statusCode = statusCode + this.coverageType = CoverageTypeV1.mapTo(coverageType) } }, processAddress = { line, postalCode, city -> @@ -192,37 +199,32 @@ class TaskLocalDataSource( this.denominator = denominator } }, - processIngredient = { text, form, number, amount, strength -> + processIngredient = { text, form, identifier, amount, strength -> IngredientEntityV1().apply { this.text = text this.form = form this.number = number this.amount = amount + this.identifier = identifier.toIdentifierEntityV1() this.strength = strength } }, - processMedication = { text, - medicationProfile, - medicationCategory, - form, - amount, - vaccine, - manufacturingInstructions, - packaging, - normSizeCode, - uniqueIdentifier, - ingredients, - _, - _ -> + processMedication = { + text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + _, + ingredients, + _, + _ -> MedicationEntityV1().apply { this.text = text ?: "" - this.medicationProfile = when (medicationProfile) { - MedicationProfile.PZN -> MedicationProfileV1.PZN - MedicationProfile.COMPOUNDING -> MedicationProfileV1.COMPOUNDING - MedicationProfile.INGREDIENT -> MedicationProfileV1.INGREDIENT - MedicationProfile.FREETEXT -> MedicationProfileV1.FREETEXT - else -> MedicationProfileV1.UNKNOWN - } this.medicationCategory = when (medicationCategory) { MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL @@ -238,7 +240,7 @@ class TaskLocalDataSource( this.manufacturingInstructions = manufacturingInstructions this.packaging = packaging this.normSizeCode = normSizeCode - this.uniqueIdentifier = uniqueIdentifier + this.identifier = identifier.toIdentifierEntityV1() this.ingredients = ingredients.toRealmList() } }, @@ -327,12 +329,30 @@ class TaskLocalDataSource( } } - suspend fun saveMedicationDispense(taskId: String, bundle: JsonElement) { - realm.tryWrite { - queryFirst("taskId = $0", taskId)?.let { syncedTask -> + private fun TaskStatus.toTaskStatusV1(): TaskStatusV1 { + return when (this) { + TaskStatus.Ready -> TaskStatusV1.Ready + TaskStatus.InProgress -> TaskStatusV1.InProgress + TaskStatus.Completed -> TaskStatusV1.Completed + TaskStatus.Draft -> TaskStatusV1.Draft + TaskStatus.Requested -> TaskStatusV1.Requested + TaskStatus.Received -> TaskStatusV1.Received + TaskStatus.Accepted -> TaskStatusV1.Accepted + TaskStatus.Rejected -> TaskStatusV1.Rejected + TaskStatus.Canceled -> TaskStatusV1.Canceled + TaskStatus.OnHold -> TaskStatusV1.OnHold + TaskStatus.Failed -> TaskStatusV1.Failed + else -> TaskStatusV1.Other + } + } - extractMedicationDispense( - bundle, + suspend fun saveMedicationDispensesWithMedications(taskId: String, bundle: JsonElement) { + realm.writeToRealm("taskId = $0", taskId) { syncedTask -> + val dispensesWithMedications = extractMedicationDispensePairs(bundle) + dispensesWithMedications.forEach { (dispense, medication) -> + extractMedicationDispenseWithMedication( + dispense, + medication, quantityFn = { value, unit -> QuantityEntityV1().apply { this.value = value @@ -345,17 +365,17 @@ class TaskLocalDataSource( this.denominator = denominator } }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> IngredientEntityV1().apply { this.text = text this.form = form this.number = number + this.identifier = identifier.toIdentifierEntityV1() this.amount = amount this.strength = strength } }, processMedication = { text, - medicationProfile, medicationCategory, form, amount, @@ -363,19 +383,13 @@ class TaskLocalDataSource( manufacturingInstructions, packaging, normSizeCode, - uniqueIdentifier, + identifier, + ingredientMedications, ingredients, lotNumber, expirationDate -> MedicationEntityV1().apply { this.text = text ?: "" - this.medicationProfile = when (medicationProfile) { - MedicationProfile.PZN -> MedicationProfileV1.PZN - MedicationProfile.COMPOUNDING -> MedicationProfileV1.COMPOUNDING - MedicationProfile.INGREDIENT -> MedicationProfileV1.INGREDIENT - MedicationProfile.FREETEXT -> MedicationProfileV1.FREETEXT - else -> MedicationProfileV1.UNKNOWN - } this.medicationCategory = when (medicationCategory) { MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL @@ -390,13 +404,14 @@ class TaskLocalDataSource( this.manufacturingInstructions = manufacturingInstructions this.packaging = packaging this.normSizeCode = normSizeCode - this.uniqueIdentifier = uniqueIdentifier + this.identifier = identifier.toIdentifierEntityV1() + this.ingredientMedications = ingredientMedications.toRealmList() this.ingredients = ingredients.toRealmList() this.lotNumber = lotNumber this.expirationDate = expirationDate } }, - processMedicationDispense = { dispenseId, patientIdentifier, medication, + processMedicationDispense = { dispenseId, patientIdentifier, med, wasSubstituted, dosageInstruction, performer, whenHandedOver -> if (query("dispenseId = $0", dispenseId) @@ -406,7 +421,7 @@ class TaskLocalDataSource( syncedTask.medicationDispenses += MedicationDispenseEntityV1().apply { this.dispenseId = dispenseId this.patientIdentifier = patientIdentifier - this.medication = medication + this.medication = med this.wasSubstituted = wasSubstituted this.dosageInstruction = dosageInstruction this.performer = performer @@ -418,6 +433,91 @@ class TaskLocalDataSource( } } } + + suspend fun saveMedicationDispense(taskId: String, bundle: JsonElement) { + realm.writeToRealm("taskId = $0", taskId) { syncedTask -> + + extractMedicationDispense( + bundle, + quantityFn = { value, unit -> + QuantityEntityV1().apply { + this.value = value + this.unit = unit + } + }, + ratioFn = { numerator, denominator -> + RatioEntityV1().apply { + this.numerator = numerator + this.denominator = denominator + } + }, + ingredientFn = { text, form, identifier, amount, strength -> + IngredientEntityV1().apply { + this.text = text + this.form = form + this.number = number + this.identifier = identifier.toIdentifierEntityV1() + this.amount = amount + this.strength = strength + } + }, + processMedication = { text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + ingredientMedications, + ingredients, + lotNumber, + expirationDate -> + MedicationEntityV1().apply { + this.text = text ?: "" + this.medicationCategory = when (medicationCategory) { + MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> + MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL + + MedicationCategory.BTM -> MedicationCategoryV1.BTM + MedicationCategory.AMVV -> MedicationCategoryV1.AMVV + else -> MedicationCategoryV1.UNKNOWN + } + this.form = form + this.amount = amount + this.vaccine = vaccine + this.manufacturingInstructions = manufacturingInstructions + this.packaging = packaging + this.normSizeCode = normSizeCode + this.ingredientMedications = ingredientMedications.toRealmList() + this.identifier = identifier.toIdentifierEntityV1() + this.ingredients = ingredients.toRealmList() + this.lotNumber = lotNumber + this.expirationDate = expirationDate + } + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, + wasSubstituted, dosageInstruction, performer, whenHandedOver -> + + if (query("dispenseId = $0", dispenseId) + .count() + .find() == 0L + ) { + syncedTask.medicationDispenses += MedicationDispenseEntityV1().apply { + this.dispenseId = dispenseId + this.patientIdentifier = patientIdentifier + this.medication = medication + this.wasSubstituted = wasSubstituted + this.dosageInstruction = dosageInstruction + this.performer = performer + this.handedOverOn = whenHandedOver + } + } + } + ) + } + } } fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = @@ -463,7 +563,8 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = ), insuranceInformation = SyncedTaskData.InsuranceInformation( name = this.insuranceInformation?.name, - status = this.insuranceInformation?.statusCode + status = this.insuranceInformation?.statusCode, + coverageType = SyncedTaskData.CoverageType.mapTo(this.insuranceInformation?.coverageType?.name) ), expiresOn = this.expiresOn?.toInstant(), acceptUntil = this.acceptUntil?.toInstant(), @@ -497,12 +598,12 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = dosageInstruction = this.medicationRequest?.dosageInstruction, multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo( indicator = this.medicationRequest?.multiplePrescriptionInfo?.indicator ?: false, - numbering = SyncedTaskData.Ratio( - numerator = SyncedTaskData.Quantity( + numbering = Ratio( + numerator = Quantity( value = this.medicationRequest?.multiplePrescriptionInfo?.numbering?.numerator?.value ?: "", unit = "" ), - denominator = SyncedTaskData.Quantity( + denominator = Quantity( value = this.medicationRequest?.multiplePrescriptionInfo?.numbering?.denominator?.value ?: "", unit = "" ) @@ -519,6 +620,7 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = note = this.medicationRequest?.note, bvg = this.medicationRequest?.bvg ), + lastMedicationDispense = this.lastMedicationDispense?.toInstant(), medicationDispenses = this.medicationDispenses.map { medicationDispense -> SyncedTaskData.MedicationDispense( dispenseId = medicationDispense.dispenseId, @@ -536,66 +638,43 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = ) fun MedicationEntityV1?.toMedication(): SyncedTaskData.Medication? = - when (this?.medicationProfile) { - MedicationProfileV1.PZN -> SyncedTaskData.MedicationPZN( - uniqueIdentifier = this.uniqueIdentifier ?: "", - category = this.medicationCategory.toMedicationCategory(), - vaccine = this.vaccine, - text = this.text, - form = this.form, - lotNumber = this.lotNumber, - expirationDate = this.expirationDate, - normSizeCode = this.normSizeCode, - amount = this.amount.toRatio() - ) - - MedicationProfileV1.COMPOUNDING -> SyncedTaskData.MedicationCompounding( - category = this.medicationCategory.toMedicationCategory(), - vaccine = this.vaccine, - text = this.text, - form = this.form, - lotNumber = this.lotNumber, - expirationDate = this.expirationDate, - manufacturingInstructions = this.manufacturingInstructions, - packaging = this.packaging, - amount = this.amount.toRatio(), - ingredients = this.ingredients.toIngredients() - ) - - MedicationProfileV1.INGREDIENT -> SyncedTaskData.MedicationIngredient( - category = this.medicationCategory.toMedicationCategory(), - vaccine = this.vaccine, - text = this.text, - form = this.form, - lotNumber = this.lotNumber, - expirationDate = this.expirationDate, - normSizeCode = this.normSizeCode, - amount = this.amount.toRatio(), - ingredients = this.ingredients.toIngredients() - ) - - MedicationProfileV1.FREETEXT -> SyncedTaskData.MedicationFreeText( - category = this.medicationCategory.toMedicationCategory(), - vaccine = this.vaccine, - text = this.text, - form = this.form, - lotNumber = this.lotNumber, - expirationDate = this.expirationDate + this?.let { medication -> + SyncedTaskData.Medication( + identifier = medication.identifier?.toIdentifier() ?: SyncedTaskData.Identifier(), + category = medication.medicationCategory.toMedicationCategory(), + vaccine = medication.vaccine, + text = medication.text, + form = medication.form, + normSizeCode = medication.normSizeCode, + amount = medication.amount.toRatio(), + manufacturingInstructions = medication.manufacturingInstructions, + packaging = medication.packaging, + ingredients = medication.ingredients.toIngredients(), + lotNumber = medication.lotNumber, + ingredientMedications = medication.ingredientMedications.map { + it.toMedication() + }, + expirationDate = medication.expirationDate ) - - else -> null } -private fun RatioEntityV1?.toRatio(): SyncedTaskData.Ratio? = this?.let { - SyncedTaskData.Ratio( +fun IdentifierEntityV1.toIdentifier(): SyncedTaskData.Identifier = SyncedTaskData.Identifier( + pzn = this.pzn, + atc = this.atc, + ask = this.ask, + snomed = this.snomed +) + +fun RatioEntityV1?.toRatio(): Ratio? = this?.let { + Ratio( numerator = it.numerator?.let { quantity -> - SyncedTaskData.Quantity( + Quantity( value = quantity.value, unit = quantity.unit ) }, denominator = it.denominator?.let { quantity -> - SyncedTaskData.Quantity( + Quantity( value = quantity.value, unit = quantity.unit ) @@ -652,7 +731,7 @@ fun ScannedTaskEntityV1.toScannedTask() = ScannedTaskData.ScannedTask( profileId = this.parent!!.id, taskId = this.taskId, - name = this.name, + name = this.name ?: "", index = this.index, accessCode = this.accessCode, scannedOn = this.scannedOn.toInstant(), diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRemoteDataSource.kt index 681d0b6e..d44b5f80 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt index 3c0be663..5fb8aaec 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.prescription.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/mapper/ProfileDataMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/mapper/ProfileDataMapper.kt new file mode 100644 index 00000000..ba05bdea --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/mapper/ProfileDataMapper.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.mapper + +import de.gematik.ti.erp.app.db.entities.v1.AvatarFigureV1 +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 +import de.gematik.ti.erp.app.db.entities.v1.ProfileColorNamesV1 +import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 +import de.gematik.ti.erp.app.db.toInstant +import de.gematik.ti.erp.app.idp.repository.toSingleSignOnTokenScope +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.SetToActiveProfile + +@Suppress("CyclomaticComplexMethod") +fun ProfileEntityV1.toProfileData( + state: SetToActiveProfile = SetToActiveProfile.NoChange +): ProfilesData.Profile = + ProfilesData.Profile( + id = this.id, + color = when (this.color) { + ProfileColorNamesV1.SPRING_GRAY -> ProfilesData.ProfileColorNames.SPRING_GRAY + ProfileColorNamesV1.SUN_DEW -> ProfilesData.ProfileColorNames.SUN_DEW + ProfileColorNamesV1.PINK -> ProfilesData.ProfileColorNames.PINK + ProfileColorNamesV1.TREE -> ProfilesData.ProfileColorNames.TREE + ProfileColorNamesV1.BLUE_MOON -> ProfilesData.ProfileColorNames.BLUE_MOON + }, + avatar = when (this.avatarFigure) { + AvatarFigureV1.PersonalizedImage -> ProfilesData.Avatar.PersonalizedImage + AvatarFigureV1.FemaleDoctor -> ProfilesData.Avatar.FemaleDoctor + AvatarFigureV1.WomanWithHeadScarf -> ProfilesData.Avatar.WomanWithHeadScarf + AvatarFigureV1.Grandfather -> ProfilesData.Avatar.Grandfather + AvatarFigureV1.BoyWithHealthCard -> ProfilesData.Avatar.BoyWithHealthCard + AvatarFigureV1.OldManOfColor -> ProfilesData.Avatar.OldManOfColor + AvatarFigureV1.WomanWithPhone -> ProfilesData.Avatar.WomanWithPhone + AvatarFigureV1.Grandmother -> ProfilesData.Avatar.Grandmother + AvatarFigureV1.ManWithPhone -> ProfilesData.Avatar.ManWithPhone + AvatarFigureV1.WheelchairUser -> ProfilesData.Avatar.WheelchairUser + AvatarFigureV1.Baby -> ProfilesData.Avatar.Baby + AvatarFigureV1.MaleDoctorWithPhone -> ProfilesData.Avatar.MaleDoctorWithPhone + AvatarFigureV1.FemaleDoctorWithPhone -> ProfilesData.Avatar.FemaleDoctorWithPhone + AvatarFigureV1.FemaleDeveloper -> ProfilesData.Avatar.FemaleDeveloper + }, + image = this.personalizedImage, + name = this.name, + insurantName = this.insurantName ?: "", + insuranceIdentifier = this.insuranceIdentifier, + insuranceName = this.insuranceName, + insuranceType = when (this.insuranceType) { + InsuranceTypeV1.GKV -> ProfilesData.InsuranceType.GKV + InsuranceTypeV1.PKV -> ProfilesData.InsuranceType.PKV + InsuranceTypeV1.None -> ProfilesData.InsuranceType.None + }, + isConsentDrawerShown = this.isConsentDrawerShown, + lastAuthenticated = this.lastAuthenticated?.toInstant(), + lastAuditEventSynced = this.lastAuditEventSynced?.toInstant(), + lastTaskSynced = this.lastTaskSynced?.toInstant(), + active = when (state) { + SetToActiveProfile.NoChange -> this.active + is SetToActiveProfile.ChangeActiveState -> true + }, + singleSignOnTokenScope = this.idpAuthenticationData?.toSingleSignOnTokenScope() + ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt index 03d98bd1..925d46f3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt @@ -1,28 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.model import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Clock import kotlinx.datetime.Instant object ProfilesData { + enum class Avatar { PersonalizedImage, FemaleDoctor, @@ -37,13 +39,19 @@ object ProfilesData { Baby, MaleDoctorWithPhone, FemaleDoctorWithPhone, - FemaleDeveloper + FemaleDeveloper; + + companion object { + val lastIndex = entries.size - 1 + const val firstIndex = 0 + } } + data class Profile( val id: ProfileIdentifier, val color: ProfileColorNames, val avatar: Avatar, - val personalizedImage: ByteArray? = null, + val image: ByteArray? = null, val name: String, val insurantName: String? = null, val insuranceIdentifier: String? = null, @@ -56,12 +64,13 @@ object ProfilesData { val active: Boolean = false, val singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? ) { - + //region Code block required due to using ByteArray in data class @Suppress("ktlint:max-line-length") override fun toString(): String { return "Profile(id='$id', color=$color, name='$name', insurantName=$insurantName, insuranceIdentifier=$insuranceIdentifier, insuranceName=$insuranceName, lastAuthenticated=$lastAuthenticated, lastAuditEventSynced=$lastAuditEventSynced, lastTaskSynced=$lastTaskSynced, active=$active, singleSignOnTokenScope=$singleSignOnTokenScope)" } + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -70,10 +79,10 @@ object ProfilesData { if (id != other.id) return false if (avatar != other.avatar) return false - if (personalizedImage != null) { - if (other.personalizedImage == null) return false - if (!personalizedImage.contentEquals(other.personalizedImage)) return false - } else if (other.personalizedImage != null) return false + if (image != null) { + if (other.image == null) return false + if (!image.contentEquals(other.image)) return false + } else if (other.image != null) return false if (name != other.name) return false if (insurantName != other.insurantName) return false if (insuranceIdentifier != other.insuranceIdentifier) return false @@ -90,7 +99,7 @@ object ProfilesData { var result = id.hashCode() result = 31 * result + color.hashCode() result = 31 * result + avatar.hashCode() - result = 31 * result + (personalizedImage?.contentHashCode() ?: 0) + result = 31 * result + (image?.contentHashCode() ?: 0) result = 31 * result + name.hashCode() result = 31 * result + (insurantName?.hashCode() ?: 0) result = 31 * result + (insuranceIdentifier?.hashCode() ?: 0) @@ -102,6 +111,9 @@ object ProfilesData { result = 31 * result + (singleSignOnTokenScope?.hashCode() ?: 0) return result } + + fun isSSOTokenValid(now: Instant = Clock.System.now()) = singleSignOnTokenScope?.token?.isValid(now) ?: false + //endregion } enum class ProfileColorNames { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt index cb33031e..337c77b6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.repository @@ -24,17 +24,20 @@ import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileColorNamesV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 import de.gematik.ti.erp.app.db.queryFirst -import de.gematik.ti.erp.app.db.toInstant import de.gematik.ti.erp.app.db.toRealmInstant -import de.gematik.ti.erp.app.idp.repository.toSingleSignOnTokenScope +import de.gematik.ti.erp.app.profiles.mapper.toProfileData import de.gematik.ti.erp.app.profiles.model.ProfilesData import io.realm.kotlin.Realm +import io.realm.kotlin.ext.asFlow import io.realm.kotlin.ext.query import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -43,6 +46,11 @@ import kotlinx.datetime.Instant // TODO: Move to value class typealias ProfileIdentifier = String +sealed interface SetToActiveProfile { + data object NoChange : SetToActiveProfile + data object ChangeActiveState : SetToActiveProfile +} + class KVNRAlreadyAssignedException( message: String, val isActiveProfile: Boolean, @@ -56,59 +64,18 @@ class DefaultProfilesRepository( ) : ProfileRepository { private val lock = Mutex() - override fun profiles() = + override fun profiles(): Flow> = realm.query().asFlow().mapNotNull { val hasActiveProfile = it.list.any { profile -> profile.active } - it.list.mapIndexed { index, profile -> - ProfilesData.Profile( - id = profile.id, - color = when (profile.color) { - ProfileColorNamesV1.SPRING_GRAY -> ProfilesData.ProfileColorNames.SPRING_GRAY - ProfileColorNamesV1.SUN_DEW -> ProfilesData.ProfileColorNames.SUN_DEW - ProfileColorNamesV1.PINK -> ProfilesData.ProfileColorNames.PINK - ProfileColorNamesV1.TREE -> ProfilesData.ProfileColorNames.TREE - ProfileColorNamesV1.BLUE_MOON -> ProfilesData.ProfileColorNames.BLUE_MOON - }, - avatar = when (profile.avatarFigure) { - AvatarFigureV1.PersonalizedImage -> ProfilesData.Avatar.PersonalizedImage - AvatarFigureV1.FemaleDoctor -> ProfilesData.Avatar.FemaleDoctor - AvatarFigureV1.WomanWithHeadScarf -> ProfilesData.Avatar.WomanWithHeadScarf - AvatarFigureV1.Grandfather -> ProfilesData.Avatar.Grandfather - AvatarFigureV1.BoyWithHealthCard -> ProfilesData.Avatar.BoyWithHealthCard - AvatarFigureV1.OldManOfColor -> ProfilesData.Avatar.OldManOfColor - AvatarFigureV1.WomanWithPhone -> ProfilesData.Avatar.WomanWithPhone - AvatarFigureV1.Grandmother -> ProfilesData.Avatar.Grandmother - AvatarFigureV1.ManWithPhone -> ProfilesData.Avatar.ManWithPhone - AvatarFigureV1.WheelchairUser -> ProfilesData.Avatar.WheelchairUser - AvatarFigureV1.Baby -> ProfilesData.Avatar.Baby - AvatarFigureV1.MaleDoctorWithPhone -> ProfilesData.Avatar.MaleDoctorWithPhone - AvatarFigureV1.FemaleDoctorWithPhone -> ProfilesData.Avatar.FemaleDoctorWithPhone - AvatarFigureV1.FemaleDeveloper -> ProfilesData.Avatar.FemaleDeveloper - }, - personalizedImage = profile.personalizedImage, - name = profile.name, - insurantName = profile.insurantName ?: "", - insuranceIdentifier = profile.insuranceIdentifier, - insuranceName = profile.insuranceName, - insuranceType = when (profile.insuranceType) { - InsuranceTypeV1.GKV -> ProfilesData.InsuranceType.GKV - InsuranceTypeV1.PKV -> ProfilesData.InsuranceType.PKV - InsuranceTypeV1.None -> ProfilesData.InsuranceType.None - }, - isConsentDrawerShown = profile.isConsentDrawerShown, - lastAuthenticated = profile.lastAuthenticated?.toInstant(), - lastAuditEventSynced = profile.lastAuditEventSynced?.toInstant(), - lastTaskSynced = profile.lastTaskSynced?.toInstant(), - // TODO change architecture of active profile - active = if (!hasActiveProfile && index == 0) { - true - } else { - profile.active - }, - singleSignOnTokenScope = profile.idpAuthenticationData?.toSingleSignOnTokenScope() + val state = if (it.list.size == 1 && !hasActiveProfile) { + SetToActiveProfile.ChangeActiveState + } else { + SetToActiveProfile.NoChange + } - ) + it.list.map { profile -> + profile.toProfileData(state) } }.flowOn(dispatcher) @@ -128,13 +95,28 @@ class DefaultProfilesRepository( ProfileEntityV1().apply { this.name = profileName this.active = activate - this.color = ProfileColorNamesV1.values().random() + this.color = ProfileColorNamesV1.entries.toTypedArray().random() + } + ) + } + } + + override suspend fun createNewProfile(profileName: String) { + realm.write { + query().find().forEach { + it.active = false + } + + copyToRealm( + ProfileEntityV1().apply { + this.name = profileName + this.active = true + this.color = ProfileColorNamesV1.entries.toTypedArray().random() } ) } } - // tag::SwitchActiveProfileRepository[] override suspend fun activateProfile(profileId: ProfileIdentifier) { realm.write { query("id != $0", profileId).find().forEach { @@ -145,15 +127,24 @@ class DefaultProfilesRepository( } } } - // end::SwitchActiveProfileRepository[] - override suspend fun removeProfile(profileId: ProfileIdentifier) { + override suspend fun removeProfile(profileId: ProfileIdentifier, profileName: String) { lock.withLock { realm.writeBlocking { val profiles = query().find() if (profiles.size == 1) { - error("Can't remove the last profile!") + // create new default profile before deleting the last profile + query().find().forEach { + it.active = false + } + copyToRealm( + ProfileEntityV1().apply { + this.name = profileName + this.active = true + this.color = ProfileColorNamesV1.entries.toTypedArray().random() + } + ) } queryFirst("id = $0", profileId)?.let { profileToDelete -> @@ -162,7 +153,6 @@ class DefaultProfilesRepository( findLatest(it)?.active = true } } - deleteAll(profileToDelete) } } @@ -205,6 +195,9 @@ class DefaultProfilesRepository( this.insuranceName = insuranceName this.insuranceIdentifier = insuranceIdentifier this.insurantName = insurantName + if (this.isNewlyCreated) { + this.name = insurantName + } } } } @@ -214,6 +207,7 @@ class DefaultProfilesRepository( realm.write { queryFirst("id = $0", profileId)?.apply { this.name = profileName + this.isNewlyCreated = false } } } @@ -240,6 +234,7 @@ class DefaultProfilesRepository( } } + @Suppress("CyclomaticComplexMethod") override suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { realm.write { queryFirst("id = $0", profileId)?.apply { @@ -280,21 +275,36 @@ class DefaultProfilesRepository( } } - override suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { - realm.write { + override suspend fun switchProfileToPKV(profileId: ProfileIdentifier): Boolean { + val entity = realm.write { queryFirst("id = $0", profileId)?.apply { this.insuranceType = InsuranceTypeV1.PKV } } + return entity?.insuranceType == InsuranceTypeV1.PKV + } + + override suspend fun switchProfileToGKV(profileId: ProfileIdentifier): Boolean { + val entity = realm.write { + queryFirst("id = $0", profileId)?.apply { + this.insuranceType = InsuranceTypeV1.GKV + } + } + return entity?.insuranceType == InsuranceTypeV1.GKV } override suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean = getProfileById(profileId).first().insuranceType == ProfilesData.InsuranceType.PKV - private fun getProfileById(profileId: ProfileIdentifier): Flow = - profiles().mapNotNull { profiles -> - profiles.find { - it.id == profileId - } - } + override fun getProfileById( + profileId: ProfileIdentifier + ): Flow = + realm.queryFirst("id = $0", profileId)?.asFlow()?.mapNotNull { + it.obj?.toProfileData() + } ?: emptyFlow() + + override suspend fun isSsoTokenValid(profileId: ProfileIdentifier): Flow = + realm.queryFirst("id = $0", profileId) + ?.asFlow()?.mapNotNull { it.obj?.toProfileData() } + ?.map { it.isSSOTokenValid() } ?: flowOf(false) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt index c21c65e3..37044bb2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.repository @@ -25,9 +25,12 @@ import kotlinx.datetime.Instant interface ProfileRepository { fun profiles(): Flow> fun activeProfile(): Flow + fun getProfileById(profileId: ProfileIdentifier): Flow + suspend fun isSsoTokenValid(profileId: ProfileIdentifier): Flow suspend fun saveProfile(profileName: String, activate: Boolean) + suspend fun createNewProfile(profileName: String) suspend fun activateProfile(profileId: ProfileIdentifier) - suspend fun removeProfile(profileId: ProfileIdentifier) + suspend fun removeProfile(profileId: ProfileIdentifier, profileName: String) suspend fun saveInsuranceInformation( profileId: ProfileIdentifier, insurantName: String, @@ -40,6 +43,7 @@ interface ProfileRepository { suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) - suspend fun switchProfileToPKV(profileId: ProfileIdentifier) + suspend fun switchProfileToPKV(profileId: ProfileIdentifier): Boolean + suspend fun switchProfileToGKV(profileId: ProfileIdentifier): Boolean suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt new file mode 100644 index 00000000..dbd91dec --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.profiles.usecase.mapper + +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +fun ProfilesData.Profile.toModel() = + ProfilesUseCaseData.Profile( + id = id, + name = name, + insurance = ProfileInsuranceInformation( + insurantName = insurantName ?: "", + insuranceIdentifier = insuranceIdentifier ?: "", + insuranceName = insuranceName ?: "", + insuranceType = when (insuranceType) { + ProfilesData.InsuranceType.None -> ProfilesUseCaseData.InsuranceType.NONE + ProfilesData.InsuranceType.GKV -> ProfilesUseCaseData.InsuranceType.GKV + ProfilesData.InsuranceType.PKV -> ProfilesUseCaseData.InsuranceType.PKV + } + ), + isActive = active, + color = color, + avatar = avatar, + image = image, + lastAuthenticated = lastAuthenticated, + ssoTokenScope = singleSignOnTokenScope + ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt index 7eadd76e..f84b28e0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt @@ -1,33 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase.model import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import kotlinx.datetime.Instant @Immutable data class PairedDevice( val name: String, val alias: String, - val connectedOn: Instant -) { - @Stable - fun isOurDevice(alias: String) = this.alias == alias -} + val connectedOn: String, + val isCurrentDevice: Boolean = false +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt index 5315e62c..43c7bfd2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt index 4a8e5b12..d3546333 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.usecase.model @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.profiles.usecase.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.model.IdpData.AlternateAuthenticationWithoutToken import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.datetime.Clock @@ -39,20 +40,30 @@ object ProfilesUseCaseData { val id: ProfileIdentifier, val name: String, val insurance: ProfileInsuranceInformation, - val active: Boolean, + val isActive: Boolean, val color: ProfilesData.ProfileColorNames, val avatar: ProfilesData.Avatar, val image: ByteArray? = null, val lastAuthenticated: Instant? = null, val ssoTokenScope: IdpData.SingleSignOnTokenScope? ) { - fun isBiometricPairing() = ssoTokenScope !is IdpData.ExternalAuthenticationToken + //region Validations required before starting processes that require tokens + fun isNotGid() = ssoTokenScope !is IdpData.ExternalAuthenticationToken + + fun isPkv() = insurance.insuranceType == InsuranceType.PKV fun isSSOTokenValid(now: Instant = Clock.System.now()) = ssoTokenScope?.token?.isValid(now) ?: false + val isDirectRedeemEnabled: Boolean + get() = lastAuthenticated == null + + fun isRedemptionAllowed() = isSSOTokenValid() || isDirectRedeemEnabled + //endregion + fun hasNoImageSelected() = this.avatar == ProfilesData.Avatar.PersonalizedImage && this.image == null + @Suppress("CyclomaticComplexMethod") override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -62,7 +73,7 @@ object ProfilesUseCaseData { if (id != other.id) return false if (name != other.name) return false if (insurance != other.insurance) return false - if (active != other.active) return false + if (isActive != other.isActive) return false if (color != other.color) return false if (avatar != other.avatar) return false if (image != null) { @@ -79,7 +90,7 @@ object ProfilesUseCaseData { var result = id.hashCode() result = 31 * result + name.hashCode() result = 31 * result + insurance.hashCode() - result = 31 * result + active.hashCode() + result = 31 * result + isActive.hashCode() result = 31 * result + color.hashCode() result = 31 * result + avatar.hashCode() result = 31 * result + (image?.contentHashCode() ?: 0) @@ -98,7 +109,6 @@ object ProfilesUseCaseData { NeverConnected } - // old state: ssoTokenScope == null && lastAuthenticated == null private fun Profile.neverConnected() = lastAuthenticated == null private fun Profile.ssoTokenSetAndConnected() = @@ -123,10 +133,16 @@ object ProfilesUseCaseData { is IdpData.AlternateAuthenticationWithoutToken -> true else -> false } - fun List.activeProfile() = first { it.active } + + fun List.activeProfile() = first { it.isActive } + fun List.profileById(id: ProfileIdentifier?) = firstOrNull { it.id == id } + fun List.containsProfileWithName(name: String) = any { it.name == name.trim() } + // for pharmacies that do not support direct redemption this condition needs to be fulfilled + fun Profile.isSsoTokenValidAndDirectRedeemEnabled() = isSSOTokenValid() && isDirectRedeemEnabled + @Stable fun Profile.connectionState(): ProfileConnectionState? = when { @@ -142,6 +158,21 @@ object ProfilesUseCaseData { else -> null } + + fun Profile.validateRequirementForLastAuthUpdateRequired( + block: (ProfileIdentifier, Instant) -> Unit + ): Profile { + when { + ssoTokenScope != null && ssoTokenScope !is AlternateAuthenticationWithoutToken && + lastAuthenticated == null -> { + ssoTokenScope.token?.let { token -> + block(id, token.validOn) + this@Companion + } + } + } + return this + } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt index 0f8a09b1..b56f1260 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt index eefe1d37..4fe4f230 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt @@ -1,27 +1,29 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.model import kotlinx.datetime.Instant +import java.util.UUID object AuditEventData { data class AuditEvent( + val uuid: String = UUID.randomUUID().toString(), val auditId: String, val taskId: String?, val description: String, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventPagingKey.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventPagingKey.kt new file mode 100644 index 00000000..7ac729da --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventPagingKey.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.protocol.model + +data class AuditEventPagingKey(val offset: Int) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventRemoteDataSource.kt index 80ae374e..d8da4fce 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt index 31531738..53b5def5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt index 63b8a3e4..43cc1fdb 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.repository +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.fhir.model.mapCatching import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull @@ -36,6 +37,11 @@ class DefaultAuditEventsRepository( private val remoteDataSource: AuditEventRemoteDataSource ) : AuditEventsRepository { + @Requirement( + "A_19177#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "The audit events are downloaded and extracted." + ) override suspend fun downloadAuditEvents( profileId: ProfileIdentifier, count: Int?, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventPagingSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventPagingSource.kt new file mode 100644 index 00000000..613c88c6 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventPagingSource.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.protocol.usecase + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.protocol.model.AuditEventPagingKey +import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import kotlin.math.max + +class AuditEventPagingSource( + private val profileId: ProfileIdentifier, + private val auditRepository: AuditEventsRepository +) : PagingSource() { + + override fun getRefreshKey( + state: PagingState + ): AuditEventPagingKey? = null + + override suspend fun load( + params: LoadParams + ): LoadResult { + val count = params.loadSize + + when (params) { + is LoadParams.Refresh -> { + return auditRepository.downloadAuditEvents( + profileId = profileId, + count = count, + offset = 0 + ) + .map { + LoadResult.Page( + data = it.auditEvents, + nextKey = if (it.bundleResultCount == AuditEventsInitialResultsPerPage) { + AuditEventPagingKey( + it.bundleResultCount + ) + } else { + null + }, + prevKey = null + ) + }.getOrElse { LoadResult.Error(it) } + } + + is LoadParams.Append -> { + return auditRepository.downloadAuditEvents( + profileId = profileId, + offset = params.key.offset, + count = count + ).map { + val actualItemCount = it.auditEvents.size + val nextKey = if (actualItemCount == count) { + AuditEventPagingKey(params.key.offset + actualItemCount) + } else { + null + } + val prevKey = if (params.key.offset == 0) null else params.key.copy( + offset = max(0, params.key.offset - count) + ) + + LoadResult.Page( + data = it.auditEvents, + nextKey = nextKey, + prevKey = prevKey, + itemsBefore = if (prevKey != null) count else 0, + itemsAfter = if (nextKey != null) count else 0 + ) + }.getOrElse { LoadResult.Error(it) } + } + + is LoadParams.Prepend -> { + // prepend is not supported + return LoadResult.Error(UnsupportedOperationException("Prepend is not supported")) + } + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt index 463a5dc9..895773e3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.usecase @@ -21,8 +21,6 @@ package de.gematik.ti.erp.app.protocol.usecase import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.PagingSource -import androidx.paging.PagingState import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.protocol.model.AuditEventData import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository @@ -30,8 +28,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.withContext -import kotlin.math.max const val AuditEventsInitialResultsPerPage = 50 const val AuditEventsNextResultsPerPage = 25 @@ -40,109 +36,19 @@ class AuditEventsUseCase( private val auditRepository: AuditEventsRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - - fun loadAuditEventsPaged( - profileId: ProfileIdentifier - ): Flow> { + operator fun invoke(profileId: ProfileIdentifier): Flow> { return Pager( PagingConfig( pageSize = AuditEventsNextResultsPerPage, initialLoadSize = AuditEventsInitialResultsPerPage, maxSize = AuditEventsInitialResultsPerPage * 2 ), - pagingSourceFactory = { AuditEventPagingSource(profileId) } - ).flow.flowOn(dispatcher) - } - suspend fun loadAuditEvents(profileId: ProfileIdentifier): List = - withContext(dispatcher) { - val initialResult = auditRepository.downloadAuditEvents( - profileId, - null, - null - ).getOrThrow() - - if (initialResult.bundleResultCount == AuditEventsInitialResultsPerPage) { - val auditEvents = initialResult.auditEvents.toMutableList() - - var offset = initialResult.bundleResultCount - var shouldContinue = true - - while (shouldContinue) { - val result = auditRepository.downloadAuditEvents( - profileId, - offset = offset, - count = AuditEventsNextResultsPerPage - ).getOrThrow() - - if (result.bundleResultCount < AuditEventsNextResultsPerPage) { - shouldContinue = false - } - - auditEvents += result.auditEvents - offset += result.bundleResultCount - } - - auditEvents - } else { - initialResult.auditEvents + pagingSourceFactory = { + AuditEventPagingSource( + profileId = profileId, + auditRepository = auditRepository + ) } - } - - data class AuditEventPagingKey(val offset: Int) - - inner class AuditEventPagingSource(private val profileId: ProfileIdentifier) : - PagingSource() { - - override fun getRefreshKey( - state: PagingState - ): AuditEventPagingKey? = null - - override suspend fun load( - params: LoadParams - ): LoadResult { - val count = params.loadSize - - when (params) { - is LoadParams.Refresh -> { - return auditRepository.downloadAuditEvents(profileId, count, 0) - .map { - LoadResult.Page( - data = it.auditEvents, - nextKey = if (it.bundleResultCount == AuditEventsInitialResultsPerPage) { - AuditEventPagingKey( - it.bundleResultCount - ) - } else { - null - }, - prevKey = null - ) - }.getOrElse { LoadResult.Error(it) } - } - - is LoadParams.Append, is LoadParams.Prepend -> { - val key = params.key!! - - return auditRepository.downloadAuditEvents(profileId, offset = key.offset, count = count).map { - val nextKey = if (it.bundleResultCount == count) { - AuditEventPagingKey( - key.offset + it.bundleResultCount - ) - } else { - null - } - val prevKey = if (key.offset == 0) null else key.copy(offset = max(0, key.offset - count)) - - LoadResult.Page( - data = it.auditEvents, - nextKey = nextKey, - prevKey = prevKey, - itemsBefore = if (prevKey != null) count else 0, - itemsAfter = if (nextKey != null) count else 0 - ) - }.getOrElse { LoadResult.Error(it) } - } - } - } + ).flow.flowOn(dispatcher) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemedPrescriptionState.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemedPrescriptionState.kt new file mode 100644 index 00000000..0715b438 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/model/RedeemedPrescriptionState.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.model + +import de.gematik.ti.erp.app.api.HttpErrorState +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +@OptIn(ExperimentalContracts::class) +sealed class RedeemedPrescriptionState { + + @OptIn(ExperimentalContracts::class) + fun isOrderCompletedState( + block: (OrderCompleted) -> Unit + ) { + contract { + returns(true) implies (this@RedeemedPrescriptionState is OrderCompleted) + returns(false) implies (this@RedeemedPrescriptionState !is OrderCompleted) + } + if (this is OrderCompleted) { + block(this) + } + } + + data object Init : RedeemedPrescriptionState() // no orders have been processed + + data class OrderCompleted( + val orderId: String, + val results: Map + ) : RedeemedPrescriptionState() // everything is processed and this contains the internal states of every prescription once they are processed + + data object Success : RedeemedPrescriptionState() + + data class Error(val errorState: HttpErrorState) : RedeemedPrescriptionState() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/DefaultRedeemLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/DefaultRedeemLocalDataSource.kt new file mode 100644 index 00000000..49857b4e --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/DefaultRedeemLocalDataSource.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.repository.datasource + +import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.toRealmInstant +import de.gematik.ti.erp.app.db.tryWrite +import io.realm.kotlin.Realm +import kotlinx.datetime.Clock + +class DefaultRedeemLocalDataSource(private val realm: Realm) : RedeemLocalDataSource { + + override suspend fun markAsRedeemed(taskId: String) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.apply { + this.redeemedOn = Clock.System.now().toRealmInstant() + } + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/RedeemLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/RedeemLocalDataSource.kt new file mode 100644 index 00000000..161cc010 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/redeem/repository/datasource/RedeemLocalDataSource.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.redeem.repository.datasource + +interface RedeemLocalDataSource { + suspend fun markAsRedeemed(taskId: String) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AnalyticsSettings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AnalyticsSettings.kt new file mode 100644 index 00000000..847bbf6b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AnalyticsSettings.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings + +import kotlinx.coroutines.flow.Flow +interface AnalyticsSettings { + fun isAnalyticsAllowed(): Flow +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AuthenticationSettings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AuthenticationSettings.kt new file mode 100644 index 00000000..d786e640 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/AuthenticationSettings.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings + +import de.gematik.ti.erp.app.settings.model.SettingsData +import kotlinx.coroutines.flow.Flow + +interface AuthenticationSettings { + val authentication: Flow + suspend fun setPassword(password: SettingsData.Authentication.Password) + suspend fun resetPassword() + suspend fun enableDeviceSecurity() + suspend fun disableDeviceSecurity() + suspend fun incrementNumberOfAuthenticationFailures() + suspend fun resetNumberOfAuthenticationFailures() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt index d2e081bc..855792e3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings @@ -28,21 +28,14 @@ interface GeneralSettings { suspend fun acceptUpdatedDataTerms(now: Instant = Clock.System.now()) suspend fun saveOnboardingData( - authenticationMode: SettingsData.AuthenticationMode, + authentication: SettingsData.Authentication, profileName: String, now: Instant = Clock.System.now() ) - - suspend fun saveAuthenticationMode(mode: SettingsData.AuthenticationMode) - val authenticationMode: Flow - suspend fun saveZoomPreference(enabled: Boolean) - suspend fun acceptInsecureDevice() - - suspend fun incrementNumberOfAuthenticationFailures() - suspend fun resetNumberOfAuthenticationFailures() suspend fun saveWelcomeDrawerShown() + suspend fun saveMainScreenTooltipShown() suspend fun acceptMlKit() suspend fun saveAllowScreenshots(allow: Boolean) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/PharmacySettings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/PharmacySettings.kt index bb121fad..81f2b7b9 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/PharmacySettings.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/PharmacySettings.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt index f5a7e2a5..7a002bd5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.settings.model import de.gematik.ti.erp.app.Requirement @@ -31,7 +33,6 @@ object SettingsData { val zoomEnabled: Boolean, val userHasAcceptedInsecureDevice: Boolean, val userHasAcceptedIntegrityNotOk: Boolean, - val authenticationFails: Int, val mlKitAccepted: Boolean, val trackingAllowed: Boolean, val screenShotsAllowed: Boolean @@ -53,17 +54,31 @@ object SettingsData { deliveryService || onlineService || openNow } - sealed class AuthenticationMode { - data object DeviceSecurity : AuthenticationMode() - class Password : AuthenticationMode { + @Requirement( + "O.Auth_7#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "App authentication is done on a domain-specific basis." + ) + data class Authentication( + val password: Password?, + val deviceSecurity: Boolean, + val failedAuthenticationAttempts: Int + ) { + val passwordIsSet: Boolean = password != null + val methodIsDeviceSecurity: Boolean = deviceSecurity && !passwordIsSet + val methodIsPassword: Boolean = passwordIsSet && !deviceSecurity + val methodIsUnspecified: Boolean = !deviceSecurity && !passwordIsSet + val bothMethodsAvailable: Boolean = deviceSecurity && passwordIsSet + val showFailedAuthenticationAttemptsError: Boolean = failedAuthenticationAttempts >= 2 + + class Password { val hash: ByteArray val salt: ByteArray @Requirement( - "O.Pass_5", + "O.Pass_5#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "Passwords are hashed with a hash function that complies with current " + - "security standards and using appropriate salts." + rationale = "Implementation of hashed password with salt as strong secure random value" ) constructor(password: String) { salt = ByteArray(32).apply { secureRandomInstance().nextBytes(this) } @@ -81,7 +96,7 @@ object SettingsData { } @Requirement( - "O.Pass_5#1", + "O.Pass_5#2", sourceSpecification = "BSI-eRp-ePA", rationale = "one-way hash function that take arbitrary-sized data and " + "output a fixed-length hash value." @@ -91,7 +106,5 @@ object SettingsData { return MessageDigest.getInstance("SHA-256").digest(combined) } } - - data object Unspecified : AuthenticationMode() } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepository.kt new file mode 100644 index 00000000..fefaa89c --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepository.kt @@ -0,0 +1,284 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.repository + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.db.entities.v1.AuthenticationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.AuthenticationPasswordEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 +import de.gematik.ti.erp.app.db.toRealmInstant +import de.gematik.ti.erp.app.db.writeToRealm +import de.gematik.ti.erp.app.settings.model.SettingsData +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +@Suppress("TooManyFunctions") +class DefaultSettingsRepository( + private val dispatchers: CoroutineDispatcher = Dispatchers.IO, + private val realm: Realm +) : SettingsRepository( + dispatchers = dispatchers, + realm = realm +) { + private val settings: Flow + get() = realm.query().first().asFlow().map { it.obj } + + override val general: Flow + get() = realm.query().first().asFlow().mapNotNull { query -> + query.obj?.let { + SettingsData.General( + latestAppVersion = SettingsData.AppVersion( + code = it.latestAppVersionCode, + name = it.latestAppVersionName + ), + onboardingShownIn = if (it.onboardingLatestAppVersionCode != -1) { + SettingsData.AppVersion( + code = it.onboardingLatestAppVersionCode, + name = it.onboardingLatestAppVersionName + ) + } else { + null + }, + welcomeDrawerShown = it.welcomeDrawerShown, + zoomEnabled = it.zoomEnabled, + userHasAcceptedInsecureDevice = it.userHasAcceptedInsecureDevice, + mainScreenTooltipsShown = it.mainScreenTooltipsShown, + mlKitAccepted = it.mlKitAccepted, + screenShotsAllowed = it.screenshotsAllowed, + trackingAllowed = it.trackingAllowed, + userHasAcceptedIntegrityNotOk = it.userHasAcceptedIntegrityNotOk + ) + } + }.flowOn(dispatchers) + + @Requirement( + "A_24525#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Check if analytics is allowed." + ) + override fun isAnalyticsAllowed(): Flow = realm.query().asFlow().map { it.list.first().trackingAllowed } + + override val pharmacySearch: Flow + get() = settings.mapNotNull { settings -> + settings?.pharmacySearch?.let { + SettingsData.PharmacySearch( + name = it.name, + locationEnabled = it.locationEnabled, + deliveryService = it.filterDeliveryService, + onlineService = it.filterOnlineService, + openNow = it.filterOpenNow + ) + } + }.flowOn(dispatchers) + + // TODO move to PharmacySearch + override suspend fun savePharmacySearch(search: SettingsData.PharmacySearch) { + writeToRealm { + this.pharmacySearch?.apply { + this.name = search.name + this.locationEnabled = search.locationEnabled + this.filterDeliveryService = search.deliveryService + this.filterOnlineService = search.onlineService + this.filterOpenNow = search.openNow + } + } + } + + override suspend fun saveZoomPreference(enabled: Boolean) { + writeToRealm { + this.zoomEnabled = enabled + } + } + + override val authentication: Flow + get() = realm.query().asFlow().mapNotNull { query -> + query.list.first().let { + it.authentication?.let { authentication -> + SettingsData.Authentication( + password = authentication.password?.let { pw -> + SettingsData.Authentication.Password( + hash = pw.hash, + salt = pw.salt + ) + }, + deviceSecurity = authentication.deviceSecurity, + failedAuthenticationAttempts = authentication.failedAuthenticationAttempts + ) + } + } + }.flowOn(dispatchers) + + private fun SettingsEntityV1.setAuthentication(authentication: SettingsData.Authentication) { + this.authentication = AuthenticationEntityV1().apply { + this.deviceSecurity = authentication.deviceSecurity + this.failedAuthenticationAttempts = authentication.failedAuthenticationAttempts + this.password = authentication.password?.let { + AuthenticationPasswordEntityV1().apply { + this.hash = it.hash + this.salt = it.salt + } + } + } + } + + override suspend fun saveOnboardingData( + authentication: SettingsData.Authentication, + profileName: String, + now: Instant + ) { + withContext(dispatchers) { + realm.writeToRealm { settings -> + copyToRealm( + ProfileEntityV1().apply { + this.name = profileName + this.active = true + this.isNewlyCreated = true + } + ) + settings.setAuthentication(authentication) + settings.setAcceptedUpdatedDataTerms(now) + settings.setOnboardingAppVersion() + } + } + } + + override suspend fun enableDeviceSecurity() { + writeToRealm { + this.authentication?.let { it.deviceSecurity = true } + } + } + + override suspend fun disableDeviceSecurity() { + writeToRealm { + this.authentication?.let { it.deviceSecurity = false } + } + } + + override suspend fun setPassword(password: SettingsData.Authentication.Password) { + writeToRealm { + this.authentication?.let { + it.password = password.let { + AuthenticationPasswordEntityV1().apply { + this.hash = it.hash + this.salt = it.salt + } + } + } + } + } + + override suspend fun resetPassword() { + writeToRealm { + this.authentication?.let { it.password = null } + } + } + + @Requirement( + "O.Pass_4#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Increments the number of authentication failures when the user fails to authenticate." + ) + override suspend fun incrementNumberOfAuthenticationFailures() { + writeToRealm { + this.authentication?.let { it.failedAuthenticationAttempts += 1 } + } + } + + override suspend fun resetNumberOfAuthenticationFailures() { + writeToRealm { + this.authentication?.let { it.failedAuthenticationAttempts = 0 } + } + } + + override suspend fun saveWelcomeDrawerShown() { + writeToRealm { + this.welcomeDrawerShown = true + } + } + + override suspend fun saveMainScreenTooltipShown() { + writeToRealm { + this.mainScreenTooltipsShown = true + } + } + + override suspend fun acceptMlKit() { + writeToRealm { + this.mlKitAccepted = true + } + } + + override suspend fun saveAllowScreenshots(allow: Boolean) { + writeToRealm { + this.screenshotsAllowed = allow + } + } + + @Requirement( + "O.Purp_5#6", + sourceSpecification = "BSI-eRp-ePA", + rationale = " save allow/disallow analytics state to settings repository." + ) + @Requirement( + "A_24525#3", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Save the user's decision to allow or disallow tracking." + ) + override suspend fun saveAllowTracking(allow: Boolean) { + writeToRealm { + this.trackingAllowed = allow + } + } + + override suspend fun acceptInsecureDevice() { + writeToRealm { + this.userHasAcceptedInsecureDevice = true + } + } + + override suspend fun acceptIntegrityNotOk() { + writeToRealm { + this.userHasAcceptedIntegrityNotOk = true + } + } + + override suspend fun acceptUpdatedDataTerms(now: Instant) { + writeToRealm { + this.setAcceptedUpdatedDataTerms(now) + } + } + + private fun SettingsEntityV1.setAcceptedUpdatedDataTerms(now: Instant) { + this.dataProtectionVersionAccepted = now.toRealmInstant() + } + + private fun SettingsEntityV1.setOnboardingAppVersion() { + this.onboardingLatestAppVersionName = this.latestAppVersionName + this.onboardingLatestAppVersionCode = this.latestAppVersionCode + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepository.kt index bc3f3662..c22bbe20 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepository.kt @@ -1,247 +1,43 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.repository -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.SettingsAuthenticationMethodV1 import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 -import de.gematik.ti.erp.app.db.toRealmInstant import de.gematik.ti.erp.app.db.writeToRealm +import de.gematik.ti.erp.app.settings.AnalyticsSettings +import de.gematik.ti.erp.app.settings.AuthenticationSettings import de.gematik.ti.erp.app.settings.GeneralSettings import de.gematik.ti.erp.app.settings.PharmacySettings -import de.gematik.ti.erp.app.settings.model.SettingsData import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.datetime.Instant -@Suppress("TooManyFunctions") -class SettingsRepository constructor( - private val dispatchers: DispatchProvider, +abstract class SettingsRepository( + private val dispatchers: CoroutineDispatcher = Dispatchers.IO, private val realm: Realm -) : GeneralSettings, PharmacySettings { - private val settings: Flow - get() = realm.query().first().asFlow().map { it.obj } - - override val general: Flow - get() = realm.query().first().asFlow().mapNotNull { query -> - query.obj?.let { - SettingsData.General( - latestAppVersion = SettingsData.AppVersion( - code = it.latestAppVersionCode, - name = it.latestAppVersionName - ), - onboardingShownIn = if (it.onboardingLatestAppVersionCode != -1) { - SettingsData.AppVersion( - code = it.onboardingLatestAppVersionCode, - name = it.onboardingLatestAppVersionName - ) - } else { - null - }, - welcomeDrawerShown = it.welcomeDrawerShown, - zoomEnabled = it.zoomEnabled, - userHasAcceptedInsecureDevice = it.userHasAcceptedInsecureDevice, - authenticationFails = it.authenticationFails, - mainScreenTooltipsShown = it.mainScreenTooltipsShown, - mlKitAccepted = it.mlKitAccepted, - screenShotsAllowed = it.screenshotsAllowed, - trackingAllowed = it.trackingAllowed, - userHasAcceptedIntegrityNotOk = it.userHasAcceptedIntegrityNotOk - ) - } - }.flowOn(dispatchers.io) - - fun isAnalyticsAllowed() = settings.mapNotNull { settings -> settings?.trackingAllowed } - - // TODO: Not used - override val authenticationMode: Flow - get() = realm.query().first().asFlow().mapNotNull { query -> - query.obj?.let { - when (it.authenticationMethod) { - SettingsAuthenticationMethodV1.DeviceSecurity -> SettingsData.AuthenticationMode.DeviceSecurity - SettingsAuthenticationMethodV1.Password -> { - it.password?.let { pw -> - SettingsData.AuthenticationMode.Password( - hash = pw.hash, - salt = pw.salt - ) - } - } - - else -> SettingsData.AuthenticationMode.Unspecified - } - } - }.flowOn(dispatchers.io) - - // TODO: Not used - override val pharmacySearch: Flow - get() = settings.mapNotNull { settings -> - settings?.pharmacySearch?.let { - SettingsData.PharmacySearch( - name = it.name, - locationEnabled = it.locationEnabled, - deliveryService = it.filterDeliveryService, - onlineService = it.filterOnlineService, - openNow = it.filterOpenNow - ) - } - }.flowOn(dispatchers.io) - - // TODO move to PharmacySearch - override suspend fun savePharmacySearch(search: SettingsData.PharmacySearch) { - writeToRealm { - this.pharmacySearch?.apply { - this.name = search.name - this.locationEnabled = search.locationEnabled - this.filterDeliveryService = search.deliveryService - this.filterOnlineService = search.onlineService - this.filterOpenNow = search.openNow - } - } - } - - override suspend fun saveZoomPreference(enabled: Boolean) { - writeToRealm { - this.zoomEnabled = enabled - } - } - - override suspend fun saveAuthenticationMode(mode: SettingsData.AuthenticationMode) { - writeToRealm { - this.setAuthenticationMode(mode) - } - } - - private fun SettingsEntityV1.setAuthenticationMode(mode: SettingsData.AuthenticationMode) { - this.authenticationMethod = when (mode) { - SettingsData.AuthenticationMode.DeviceSecurity -> SettingsAuthenticationMethodV1.DeviceSecurity - is SettingsData.AuthenticationMode.Password -> SettingsAuthenticationMethodV1.Password - else -> SettingsAuthenticationMethodV1.Unspecified - } - if (mode is SettingsData.AuthenticationMode.Password) { - this.authenticationMethod = SettingsAuthenticationMethodV1.Password - this.password?.apply { - this.hash = mode.hash - this.salt = mode.salt - } - } else { - this.password?.reset() - } - } - - override suspend fun saveOnboardingData( - authenticationMode: SettingsData.AuthenticationMode, - profileName: String, - now: Instant - ) { - withContext(dispatchers.io) { - realm.writeToRealm { settings -> - copyToRealm( - ProfileEntityV1().apply { - this.name = profileName - this.active = true - } - ) - settings.setAuthenticationMode(authenticationMode) - settings.setAcceptedUpdatedDataTerms(now) - settings.setOnboardingAppVersion() - } - } - } - - override suspend fun incrementNumberOfAuthenticationFailures() { - writeToRealm { - this.authenticationFails += 1 - } - } - - override suspend fun resetNumberOfAuthenticationFailures() { - writeToRealm { - this.authenticationFails = 0 - } - } - - override suspend fun saveWelcomeDrawerShown() { - writeToRealm { - this.welcomeDrawerShown = true - } - } - - override suspend fun saveMainScreenTooltipShown() { - writeToRealm { - this.mainScreenTooltipsShown = true - } - } - - override suspend fun acceptMlKit() { - writeToRealm { - this.mlKitAccepted = true - } - } - - override suspend fun saveAllowScreenshots(allow: Boolean) { - writeToRealm { - this.screenshotsAllowed = allow - } - } - - override suspend fun saveAllowTracking(allow: Boolean) { - writeToRealm { - this.trackingAllowed = allow - } - } - - override suspend fun acceptInsecureDevice() { - writeToRealm { - this.userHasAcceptedInsecureDevice = true - } - } - - override suspend fun acceptIntegrityNotOk() { - writeToRealm { - this.userHasAcceptedIntegrityNotOk = true - } - } - - override suspend fun acceptUpdatedDataTerms(now: Instant) { - writeToRealm { - this.setAcceptedUpdatedDataTerms(now) - } - } - - private fun SettingsEntityV1.setAcceptedUpdatedDataTerms(now: Instant) { - this.dataProtectionVersionAccepted = now.toRealmInstant() - } - - private fun SettingsEntityV1.setOnboardingAppVersion() { - this.onboardingLatestAppVersionName = this.latestAppVersionName - this.onboardingLatestAppVersionCode = this.latestAppVersionCode - } - - private suspend fun writeToRealm(block: SettingsEntityV1.() -> Unit) { - withContext(dispatchers.io) { +) : GeneralSettings, + PharmacySettings, + AnalyticsSettings, + AuthenticationSettings { + suspend fun writeToRealm(block: SettingsEntityV1.() -> Unit) { + withContext(dispatchers) { realm.writeToRealm { it.block() } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt index ececa085..0757a35b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt index b7a6b578..ceb60342 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/DisableDeviceSecurityUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/DisableDeviceSecurityUseCase.kt new file mode 100644 index 00000000..702e110a --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/DisableDeviceSecurityUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class DisableDeviceSecurityUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.disableDeviceSecurity() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/EnableDeviceSecurityUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/EnableDeviceSecurityUseCase.kt new file mode 100644 index 00000000..1d35a29f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/EnableDeviceSecurityUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class EnableDeviceSecurityUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.enableDeviceSecurity() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationModeUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationModeUseCase.kt deleted file mode 100644 index 99b35089..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationModeUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.usecase - -import de.gematik.ti.erp.app.settings.repository.SettingsRepository - -class GetAuthenticationModeUseCase( - private val settingsRepository: SettingsRepository -) { - operator fun invoke() = settingsRepository.authenticationMode -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationUseCase.kt new file mode 100644 index 00000000..70fdc521 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetAuthenticationUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class GetAuthenticationUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke() = settingsRepository.authentication +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt index daeade3f..4db4bc3c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt @@ -1,31 +1,35 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map class GetCanStartToolTipsUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { operator fun invoke(): Flow = settingsRepository.general.map { it.welcomeDrawerShown && it.onboardingShownIn != null && !it.mainScreenTooltipsShown - } + }.flowOn(dispatcher) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt index 05ea0e2d..d2076744 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt @@ -1,31 +1,35 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map class GetMLKitAcceptedUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { operator fun invoke(): Flow = settingsRepository.general.map { it.mlKitAccepted - } + }.flowOn(dispatcher) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt index ab6241e4..29094f35 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase @@ -27,8 +27,9 @@ class GetOnboardingSucceededUseCase( private val settingsRepository: SettingsRepository ) { operator fun invoke(): Boolean = runBlocking { - settingsRepository.general.map { + val isOnboardingShown = settingsRepository.general.map { it.onboardingShownIn != null }.first() + isOnboardingShown } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt index 33eebb35..fafdaee8 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt index 16cdfd5f..514d8c31 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt @@ -1,31 +1,35 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map class GetShowWelcomeDrawerUseCase( - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { operator fun invoke(): Flow = settingsRepository.general.map { !it.welcomeDrawerShown - } + }.flowOn(dispatcher) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetZoomStateUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetZoomStateUseCase.kt index bc1ff740..6e1d1928 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetZoomStateUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetZoomStateUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/ResetPasswordUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/ResetPasswordUseCase.kt new file mode 100644 index 00000000..69ec3947 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/ResetPasswordUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class ResetPasswordUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.resetPassword() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveDeviceSecurityUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveDeviceSecurityUseCase.kt deleted file mode 100644 index 6a04ee0f..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveDeviceSecurityUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.usecase - -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository - -class SaveDeviceSecurityUseCase( - private val settingsRepository: SettingsRepository -) { - suspend operator fun invoke() = settingsRepository.saveAuthenticationMode( - SettingsData.AuthenticationMode.DeviceSecurity - ) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingDataUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingDataUseCase.kt index 97a77700..037b4fec 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingDataUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingDataUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase @@ -25,9 +25,9 @@ import kotlinx.datetime.Clock class SaveOnboardingDataUseCase( private val settingsRepository: SettingsRepository ) { - suspend operator fun invoke(authenticationMode: SettingsData.AuthenticationMode, profileName: String) = + suspend operator fun invoke(authentication: SettingsData.Authentication, profileName: String) = settingsRepository.saveOnboardingData( - authenticationMode = authenticationMode, + authentication = authentication, profileName = profileName, now = Clock.System.now() ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt deleted file mode 100644 index 01ae1352..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.usecase - -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository - -class SavePasswordUseCase( - private val settingsRepository: SettingsRepository -) { - suspend operator fun invoke(password: String) = settingsRepository.saveAuthenticationMode( - SettingsData.AuthenticationMode.Password(password = password) - ) -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt deleted file mode 100644 index d5ddfaef..00000000 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.usecase - -import de.gematik.ti.erp.app.settings.repository.SettingsRepository - -class SaveToolTippsShownUseCase( - private val settingsRepository: SettingsRepository -) { - suspend operator fun invoke() = settingsRepository.saveMainScreenTooltipShown() -} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTipsShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTipsShownUseCase.kt new file mode 100644 index 00000000..de0ccd7f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTipsShownUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SaveToolTipsShownUseCase( + private val settingsRepository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke() = withContext(dispatcher) { + settingsRepository.saveMainScreenTooltipShown() + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt index 3b07f51b..45e0574f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveZoomPreferenceUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveZoomPreferenceUseCase.kt index 33ee49bc..413d58b3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveZoomPreferenceUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveZoomPreferenceUseCase.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SetPasswordUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SetPasswordUseCase.kt new file mode 100644 index 00000000..58262d2f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SetPasswordUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class SetPasswordUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(password: String) = settingsRepository.setPassword( + password = SettingsData.Authentication.Password(password) + ) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/state/GenericState.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/state/GenericState.kt index a1d54208..55b18b55 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/state/GenericState.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/state/GenericState.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.state diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt index 13d4f606..c7d38dd0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.timeouts.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt index 3f2c93f1..5af90491 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/DMUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/DMUtils.kt index 57f7c7e7..121e3cef 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/DMUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/DMUtils.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/LetExtensions.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/LetExtensions.kt index 1050298d..ed2cc1bf 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/LetExtensions.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/LetExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/ListExtension.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/ListExtension.kt index 5b5e81e8..771d6f3f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/ListExtension.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/ListExtension.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Quad.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Quad.kt index 472a28ff..d0fbb83f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Quad.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/Quad.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt index ab3927d2..703f1406 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/StringExtensions.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt index c2d8ce2f..f2a7ca47 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils @@ -32,6 +32,7 @@ import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.atTime import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDateTime @@ -181,7 +182,7 @@ fun Instant.toStartOfDayInUTC(): Instant { return currentLocalDateTime.date.atStartOfDayIn(TimeZone.UTC) } -fun Instant.toFormattedDate(): String? { +fun Instant.toFormattedDate(): String { val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") return this.toLocalDateTime(TimeZone.currentSystemDefault()) .date.toJavaLocalDate().format(dateFormatter) @@ -233,11 +234,6 @@ fun JsonPrimitive.asFhirLocalTime(): FhirTemporal.LocalTime? = FhirTemporal.LocalTime(LocalTime.parse(it)) } -fun JsonPrimitive.asLocalDateTime(): FhirTemporal.LocalDateTime? = - this.contentOrNull?.let { - FhirTemporal.LocalDateTime(LocalDateTime.parse(it)) - } - fun JsonPrimitive.asFhirLocalDate(): FhirTemporal.LocalDate? = this.contentOrNull?.let { FhirTemporal.LocalDate(LocalDate.parse(it)) @@ -247,3 +243,28 @@ fun JsonPrimitive.asFhirInstant(): FhirTemporal.Instant? = this.contentOrNull?.let { FhirTemporal.Instant(Instant.parse(it)) } + +// TODO: find a better place/way for this +fun LocalTime.toHourMinuteString(): String { + return "${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}" +} + +fun Instant.toLocalDate() = this.toLocalDateTime(TimeZone.currentSystemDefault()).date +fun LocalDate.formattedString(): String { + val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + return this.toJavaLocalDate().format(dateFormatter) +} + +fun LocalDate.formattedStringShort(): String { + val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yy") + return this.toJavaLocalDate().format(dateFormatter) +} + +fun LocalDate.isMaxDate() = this == maxLocalDate() +fun LocalDate.isBeforeCurrentDate(currentDate: LocalDate) = this < currentDate +fun LocalDate.isInFuture(currentDate: LocalDate) = this > currentDate +fun LocalDate.atCurrentTime(now: Instant): Instant = this.atTime(now.toLocalDateTime(TimeZone.currentSystemDefault()).time).toInstant( + TimeZone.currentSystemDefault() +) + +fun maxLocalDate() = LocalDate.parse("9999-12-31") diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/BrainPoolCurves.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/BrainPoolCurves.kt new file mode 100644 index 00000000..3381b195 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/BrainPoolCurves.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.vau + +object BrainPoolCurves { + const val P256r1 = "brainpoolP256r1" +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt index 7bac510b..e4ba2587 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.vau import kotlinx.datetime.Instant diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/ClientCrypto.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/ClientCrypto.kt index 71d8dfa4..b159fc41 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/ClientCrypto.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/ClientCrypto.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("TopLevelPropertyNaming", "MagicNumber") + package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.Requirement @@ -95,14 +97,32 @@ class VauChannelSpec constructor( cryptoConfig: VauCryptoConfig = defaultCryptoConfig ): RawRequestData { + @Requirement( + "A_20161-01#10", + sourceSpecification = "gemSpec_Krypt", + rationale = "3./4. create the AES-Key.", + codeLines = 3 + ) val decryptionKey = KeyGenerator.getInstance("AES", cryptoConfig.provider).apply { init(this@VauChannelSpec.decryptionKeySize * 8) }.generateKey() + @Requirement( + "A_20161-01#9", + sourceSpecification = "gemSpec_Krypt", + rationale = "2./4. create the Request-ID.", + codeLines = 2 + ) val requestId = ByteArray(this.requestIdSize).apply { SecureRandom().nextBytes(this) } + @Requirement( + "A_20161-01#12", + sourceSpecification = "gemSpec_Krypt", + rationale = "6 a-g: encrypt Vau-Request.", + codeLines = 8 + ) return encryptRawVauRequest( innerHttp = innerHttp, bearer = bearer, @@ -117,9 +137,9 @@ class VauChannelSpec constructor( * Encrypt raw request data as the inner request. */ @Requirement( - "A_20161-01", + "A_20161-01#7", sourceSpecification = "gemSpec_Krypt", - rationale = "Request encryption" + rationale = "Encrypt raw request data as the inner request" ) fun encryptRawVauRequest( innerHttp: ByteArray, @@ -134,6 +154,12 @@ class VauChannelSpec constructor( ): RawRequestData { val symmetricalKeyHex = decryptionKey.encoded!!.toLowerCaseHex() val requestIdHex = requestId.toLowerCaseHex() + + @Requirement( + "A_20161-01#11", + sourceSpecification = "gemSpec_Krypt", + rationale = "5. Create the inner HTTP request." + ) val composedInnerHttp = composeInnerHttp(innerHttp, this.version, bearer, requestIdHex, symmetricalKeyHex) @@ -187,11 +213,15 @@ class VauChannelSpec constructor( * * @return the encrypted request and [RawRequestData] of the actual encryption process. */ - @Requirement( - "A_20161-01#5", + /*( "A_21325", sourceSpecification = "gemSpec_eRp_FdV", rationale = "Generate AES key, assemble and encrypt VAU request." + )*/ + @Requirement( + "A_20161-01#5", + sourceSpecification = "gemSpec_Krypt", + rationale = "Generate AES key, assemble and encrypt VAU request." ) fun encryptHttpRequest( innerRequest: Request, @@ -217,7 +247,11 @@ class VauChannelSpec constructor( ) val body = encryptedRawRequest.payload.toRequestBody(defaultContentType) - + @Requirement( + "A_20161-01#14", + sourceSpecification = "gemSpec_Krypt", + rationale = "8. Generate HTTPS Request" + ) return Pair( Request.Builder() .url(requireNotNull(baseUrl.resolve("VAU/$userpseudonym"))) @@ -238,8 +272,7 @@ class VauChannelSpec constructor( * @return the decrypted inner response with the user pseudonym. */ @Requirement( - "A_20174#2", - "A_20175", + "A_20175#1", sourceSpecification = "gemSpec_Krypt", rationale = "Decrypt VAU response." ) @@ -253,8 +286,19 @@ class VauChannelSpec constructor( ): Pair { require(outerResponse.isSuccessful) val body = requireNotNull(outerResponse.body) { "VAU response body empty" } + @Requirement( + "A_20174#1", + sourceSpecification = "gemSpec_Krypt", + rationale = "1. Check content type of VAU response.", + codeLines = 1 + ) require(body.contentType() == defaultContentType) { "VAU response body has wrong content type" } - + @Requirement( + "A_20174#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "2.1 Get Userpseudonym from VAU response.", + codeLines = 1 + ) val userpseudonym = outerResponse.header("Userpseudonym") val p = decryptRawVauResponse( @@ -262,7 +306,12 @@ class VauChannelSpec constructor( decryptionKey = rawRequestData.decryptionKey, cryptoConfig = cryptoConfig ) - + @Requirement( + "A_20174#5", + sourceSpecification = "gemSpec_Krypt", + rationale = "3.2/4. Check if decrypted VAU response is valid.", + codeLines = 5 + ) require(p.size >= this.minResponseSize) require(p[0] == this.version) @@ -272,7 +321,12 @@ class VauChannelSpec constructor( ) { "VAU response contains wrong request id" } val innerResponse = p.copyOfRange(this.minResponseSize, p.size) - + @Requirement( + "A_20174#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "2.2 Return the validated inner response and the user pseudonym for next usage.", + codeLines = 1 + ) return Pair(innerResponse.toVauInnerHttpResponse(previousInnerRequest), userpseudonym) } @@ -288,8 +342,6 @@ class VauChannelSpec constructor( } } -// http - /** * Create a raw http request according to rfc2616 from a okhttp [Request]. * @@ -302,6 +354,12 @@ class VauChannelSpec constructor( * * Throws an exception if [baseUrl] doesn't contain a trailing `/` or [this] doesn't contain the [baseUrl]. */ +@Requirement( + "A_20161-01#8", + sourceSpecification = "gemSpec_Krypt", + rationale = "1: Serialize the request into a String that can be interpreted by the VAU server" + + "Note: A HTTP body is only included into the string representation when it is UTF-8 encoded." +) fun Request.toRawVauInnerHttpRequest( baseUrl: HttpUrl, protocol: Protocol = Protocol.HTTP_1_1 @@ -326,7 +384,7 @@ fun Request.toRawVauInnerHttpRequest( req.headers.forEach { h -> writeUtf8("${h.first}: ${h.second}\r\n") } - writeUtf8("Content-Length: ${req.body?.contentLength() ?: 0 }\r\n") + writeUtf8("Content-Length: ${req.body?.contentLength() ?: 0}\r\n") // body separation writeUtf8("\r\n") // body if present @@ -344,6 +402,12 @@ private fun Response.Builder.parseResponseLine(l: String): Response.Builder = /** * Creates an okhttp [Response] from a raw http request according to rfc2616. */ +@Requirement( + "A_20174#5", + sourceSpecification = "gemSpec_Krypt", + rationale = "Verify decrypted message. Expect: “1 ", + codeLines = 1 +) fun String.toVauInnerHttpResponse(req: Request): Response = this.split("\r\n\r\n", limit = 2).let { rawHttp -> val rawHeader = rawHttp.first().split("\r\n").iterator() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Crypto.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Crypto.kt index 01054444..f1c10dbf 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Crypto.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Crypto.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.Requirement @@ -150,8 +152,15 @@ object Ecies { } val ivSpec = IvParameterSpec(ivBytes) + @Requirement( + "GS-A_4357-02#1", + "GS-A_4361-02#1", + sourceSpecification = "gemSpec_Krypt", + rationale = "Ensure that brainpoolP256r1 is used", + codeLines = 2 + ) val eKp = KeyPairGenerator.getInstance("EC", cryptoConfig.provider) - .apply { initialize(ECGenParameterSpec("brainpoolP256r1"), cryptoConfig.random) } + .apply { initialize(ECGenParameterSpec(BrainPoolCurves.P256r1), cryptoConfig.random) } .generateKeyPair() val cipher = @@ -172,7 +181,14 @@ object Ecies { val x = BigInteger(1, it.copyOfRange(1, 1 + 32)) val y = BigInteger(1, it.copyOfRange(1 + 32, 1 + 32 * 2)) - val curveSpec = ECNamedCurveTable.getParameterSpec("brainpoolP256r1") + @Requirement( + "GS-A_4357-02#4", + "GS-A_4361-02#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "Ensure that brainpoolP256r1 is used", + codeLines = 2 + ) + val curveSpec = ECNamedCurveTable.getParameterSpec(BrainPoolCurves.P256r1) val otherPublicKey = org.bouncycastle.jce.spec.ECPublicKeySpec( curveSpec.curve.createPoint(x, y), curveSpec @@ -225,6 +241,12 @@ object AesGcm { return ivBytes + cipher } + @Requirement( + "A_20174#4", + sourceSpecification = "gemSpec_Krypt", + rationale = "3.1 Decrypting of cipher text.", + codeLines = 1 + ) fun decrypt( aesKey: SecretKey, spec: VauAesGcmSpec, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt index 68972192..08276fb6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau +import de.gematik.ti.erp.app.Requirement import kotlinx.datetime.Instant import kotlinx.datetime.toKotlinInstant import org.bouncycastle.asn1.ASN1ObjectIdentifier @@ -80,10 +81,20 @@ fun BasicOCSPResp.checkSignatureWith(signatureCertificate: X509CertificateHolder } } +@Requirement( + "A_21218#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "Checks if the valid OCSP response is less than 12 hours old." +) /** * Checks if the field 'producedAt' plus [maxAge] is after [timestamp] and 'producedAt' is before [timestamp]. * Throws an exception if the check fails. */ +@Requirement( + "A_21218#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "Checks if the valid OCSP response is less than 12 hours old." +) fun BasicOCSPResp.checkValidity(maxAge: Duration, timestamp: Instant) { requireNotNull( this.producedAt?.toInstant()?.toKotlinInstant()?.takeIf { it + maxAge >= timestamp && timestamp >= it } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Utils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Utils.kt index d3147406..05f2f536 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Utils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/Utils.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.vau // hex @@ -46,6 +48,7 @@ fun ByteArray.toLowerCaseHex(): ByteArray { /** * Searches [other] within [this] array of bytes. */ +@Suppress("NestedBlockDepth") fun ByteArray.contains(other: ByteArray): Boolean { if (this.isEmpty() || other.isEmpty() || other.size > this.size) { return false diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/VauService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/VauService.kt index e98c864e..c9abd861 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/VauService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/VauService.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.api @@ -25,7 +25,7 @@ import retrofit2.Response import retrofit2.http.GET @Requirement( - "O.Purp_8#2", + "O.Purp_8#3", sourceSpecification = "BSI-eRp-ePA", rationale = "Interface of vau service" ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauModels.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauModels.kt index 20037adb..c767efaf 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauModels.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauModels.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:UseSerializers(OCSPSerializer::class, X509Serializer::class) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauSerializers.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauSerializers.kt index 3049f8fe..a25fd44b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauSerializers.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/api/model/VauSerializers.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.api.model diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauLocalDataSource.kt index 09bc8069..70c15d38 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauLocalDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRemoteDataSource.kt index 9a5174a6..4d70c164 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRemoteDataSource.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt index 3c5f01e7..154cc85e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.repository diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt index edb16af9..5cac12c7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau.usecase @@ -26,10 +26,15 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.hours @Requirement( - "A_20161-01#2", - sourceSpecification = "gemSpec_eRp_FdV", + "A_20161-01#6", + sourceSpecification = "gemSpec_Krypt", rationale = "Create trust store config." ) +@Requirement( + "A_21218#6", + sourceSpecification = "gemSpec_Krypt", + rationale = "Gematik Root CA 3 as trust anchor has to be setup in the program code." +) class TruststoreConfig(getTrustAnchor: () -> String) { val maxOCSPResponseAge: Duration by lazy { BuildKonfig.VAU_OCSP_RESPONSE_MAX_AGE.hours diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt index 80ba8f3f..3b4fe19c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt @@ -1,21 +1,23 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("MagicNumber", "UnusedPrivateProperty", "UnusedPrivateMember") + package de.gematik.ti.erp.app.vau.usecase import de.gematik.ti.erp.app.Requirement @@ -62,10 +64,9 @@ typealias TrustedTruststoreProvider = ( ) -> TrustedTruststore @Requirement( - "O.Auth_11", + "O.Auth_12#1", sourceSpecification = "BSI-eRp-ePA", - rationale = "We use TLS Pinning and a Trust Store for VAU communication. " + - "See NetworkModule and TruststoreUseCase for implementation." + rationale = "TruststoreUseCase implementation" ) class TruststoreUseCase( private val config: TruststoreConfig, @@ -113,7 +114,6 @@ class TruststoreUseCase( } } } - suspend fun withValidVauPublicKey(block: (vauPubKey: ECPublicKey) -> R): R = lock.withLock { val timestamp = timeSourceProvider() @@ -170,14 +170,18 @@ class TruststoreUseCase( } } + // A_20617-01, @Requirement( - "A_20161-01#4", - "A_20614#2,", - "A_20617-01#3", + "A_20161-01#2", "A_21218#2", sourceSpecification = "gemSpec_Krypt", rationale = "Create Truststore." ) + @Requirement( + "A_20623#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Create truststore" + ) private suspend fun createTrustedTruststore(timestamp: Instant): TrustedTruststore { Napier.d("Load truststore from repository...") @@ -193,6 +197,12 @@ class TruststoreUseCase( } } +@Requirement( + "A_21222#1", + sourceSpecification = "gemSpec_Krypt", + rationale = "Using the wrapper the certificate is always checked.", + codeLines = 10 +) /** * Wrapper for X.509 certificates of type [X509Certificate]. */ @@ -212,13 +222,17 @@ class TrustedTruststore private constructor( val vauPublicKey: ECPublicKey ) { + // A_20617-01, @Requirement( "A_20161-01#3", - "A_20614#1,", - "A_20617-01#2", "A_21218#1", sourceSpecification = "gemSpec_Krypt", - rationale = "Check OCSP validity." + rationale = "OCSP validity is verified by the truststore." + ) + @Requirement( + "A_20623#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "OCSP validity is verified by truststore" ) fun checkValidity(ocspResponseMaxAge: Duration, timestamp: Instant) { require(ocspResponses.isNotEmpty()) { "No OCSp responses. This should never happen" } @@ -234,10 +248,14 @@ class TrustedTruststore private constructor( } } + @Requirement( + "A_20623#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Create a TrustedTruststore." + ) @Requirement( "A_20161-01#1", - "A_20623", - sourceSpecification = "gemSpec_eRp_FdV", + sourceSpecification = "gemSpec_Krypt", rationale = "Create a TrustedTruststore." ) companion object { @@ -290,12 +308,22 @@ class TrustedTruststore private constructor( } } +@Requirement( + "A_21222#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "X509Certificates are checked before using them." +) /** * Returns a list of validated OCSP responses by signature and the provided [timestamp] & [maxAge]. * * A OCSP response is valid, if the contained certificate is verified with at least on of the * certificate chains [caCertChains] and is within the provided period of time. */ +@Requirement( + "A_21222#2", + sourceSpecification = "gemSpec_Krypt", + rationale = "X509Certificates are checked before using them." +) fun findValidOcspResponses( ocspResponses: List, caCertChains: List>, @@ -329,11 +357,21 @@ fun findValidOcspResponses( } } +@Requirement( + "A_21222#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "X509Certificates are checked before using them." +) /** * Returns the first valid VAU certificate. * While only one VAU certificate can exist at any time, this won't fail if multiple chains are valid. * Before we throw an exception with this non-critical behavior, just pick the first valid as the true one. */ +@Requirement( + "A_21222#3", + sourceSpecification = "gemSpec_Krypt", + rationale = "X509Certificates are checked before using them." +) fun findValidVauChain( chains: List>, validOcspResponses: List, @@ -351,9 +389,15 @@ fun findValidVauChain( * Returns all valid IDP certificate chains. */ @Requirement( - "A_20625", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Validate signature of IDP chains." + "A_20625#1", + "A_20623#5", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Validate signature of IDP chains. Check for OID, OCSP response and signature." +) +@Requirement( + "A_21222#4", + sourceSpecification = "gemSpec_Krypt", + rationale = "X509Certificates are checked before using them." ) fun findValidIdpChains( chains: List>, diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt index 38738a7c..688dd964 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt index e5680834..cf43a4a5 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.api @@ -25,7 +25,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import org.junit.Rule -import kotlin.test.Test +import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt index 49c39322..61efee59 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.model diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentStateTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentStateTest.kt new file mode 100644 index 00000000..e7df4d9e --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/model/ConsentStateTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.model + +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.HTTP_BAD_REQUEST +import de.gematik.ti.erp.app.api.HTTP_CONFLICT +import de.gematik.ti.erp.app.api.HTTP_FORBIDDEN +import de.gematik.ti.erp.app.api.HTTP_INTERNAL_ERROR +import de.gematik.ti.erp.app.api.HTTP_METHOD_NOT_ALLOWED +import de.gematik.ti.erp.app.api.HTTP_NOT_FOUND +import de.gematik.ti.erp.app.api.HTTP_REQUEST_TIMEOUT +import de.gematik.ti.erp.app.api.HTTP_TOO_MANY_REQUESTS +import de.gematik.ti.erp.app.api.HTTP_UNAUTHORIZED +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Test +import retrofit2.HttpException +import retrofit2.Response +import java.net.UnknownHostException +import kotlin.test.assertEquals + +class ConsentStateTest { + + @Test + fun `map consent error state Unknown`() { + val error = Throwable(message = "", cause = Throwable()) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.Unknown, state) + } + + @Test + fun `map consent error state UnknownHostException`() { + val error = Throwable(message = "", cause = Throwable("", UnknownHostException())) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.NoInternet(context), state) + } + + @Test + fun `map consent error state HTTP_CONFLICT`() { + val error = ApiCallException("", Response.error(HTTP_CONFLICT, "".toResponseBody(null))) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.AlreadyGranted, state) + } + + @Test + fun `map consent error state HTTP_REQUEST_TIMEOUT`() { + val error = ApiCallException("", Response.error(HTTP_REQUEST_TIMEOUT, "".toResponseBody(null))) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.ServerTimeout(context), state) + } + + @Test + fun `map consent error state HTTP_INTERNAL_ERROR`() { + val error = ApiCallException("", Response.error(HTTP_INTERNAL_ERROR, "".toResponseBody(null))) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.InternalError(context), state) + } + + @Test + fun `map consent error state HTTP_TOO_MANY_REQUESTS`() { + val error = ApiCallException("", Response.error(HTTP_TOO_MANY_REQUESTS, "".toResponseBody(null))) + val context = ConsentContext.GetConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.TooManyRequests(context), state) + } + + @Test + fun `map consent error state HTTP_NOT_FOUND`() { + val error = ApiCallException("", Response.error(HTTP_NOT_FOUND, "".toResponseBody(null))) + val context = ConsentContext.RevokeConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.ChargeConsentAlreadyRevoked, state) + } + + @Test + fun `map consent error state HTTP_BAD_REQUEST`() { + val error = ApiCallException("", Response.error(HTTP_BAD_REQUEST, "".toResponseBody(null))) + val context = ConsentContext.RevokeConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.BadRequest, state) + } + + @Test + fun `map consent error state HTTP_METHOD_NOT_ALLOWED`() { + val error = ApiCallException( + "", + Response.error(HTTP_METHOD_NOT_ALLOWED, "".toResponseBody(null)) + ) + val context = ConsentContext.RevokeConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.BadRequest, state) + } + + @Test + fun `map consent error state HTTP_FORBIDDEN`() { + val error = ApiCallException("", Response.error(HTTP_FORBIDDEN, "".toResponseBody(null))) + val context = ConsentContext.RevokeConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.Forbidden, state) + } + + @Test + fun `map consent error state HTTP_UNAUTHORIZED`() { + val error = ApiCallException("", Response.error(HTTP_UNAUTHORIZED, "".toResponseBody(null))) + val context = ConsentContext.RevokeConsent + val state = mapConsentErrorStates(error, context) + assertEquals(ConsentState.ConsentErrorState.Unauthorized, state) + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepositoryTest.kt index d048311b..011da549 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepositoryTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.repository diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCaseTest.kt deleted file mode 100644 index a092cec9..00000000 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecase/SaveGrantConsentDrawerShownUseCaseTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ -package de.gematik.ti.erp.app.consent.usecase - -import de.gematik.ti.erp.app.consent.repository.ConsentLocalDataSource -import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource -import de.gematik.ti.erp.app.consent.repository.ConsentRepository -import de.gematik.ti.erp.app.consent.repository.DefaultConsentRepository -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -class SaveGrantConsentDrawerShownUseCaseTest { - - private lateinit var saveGrantConsentDrawerShownUseCase: SaveGrantConsentDrawerShownUseCase - - @MockK(relaxed = true) - private lateinit var localDataSource: ConsentLocalDataSource - - @MockK - private lateinit var remoteDataSource: ConsentRemoteDataSource - - private lateinit var consentRepository: ConsentRepository - - @Before - fun setup() { - MockKAnnotations.init(this) - coEvery { localDataSource.saveGiveConsentDrawerShown("123") } returns Unit - consentRepository = DefaultConsentRepository(remoteDataSource, localDataSource) - saveGrantConsentDrawerShownUseCase = SaveGrantConsentDrawerShownUseCase(consentRepository) - } - - @Test - fun `save consent drawer shown for profileId`() = runTest { - saveGrantConsentDrawerShownUseCase("123") - coVerify(exactly = 1) { - consentRepository.saveGrantConsentDrawerShown("123") - } - coVerify(exactly = 0) { - consentRepository.saveGrantConsentDrawerShown("0") - } - } -} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GetConsentUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GetConsentUseCaseTest.kt new file mode 100644 index 00000000..c854ce0b --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GetConsentUseCaseTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.usecases + +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.repository.ConsentLocalDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.consent.repository.DefaultConsentRepository +import de.gematik.ti.erp.app.consent.usecase.GetConsentUseCase +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class GetConsentUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + private val remoteDataSource = mockk() + private val localDataSource = mockk() + private lateinit var repository: ConsentRepository + private lateinit var useCase: GetConsentUseCase + + @Before + fun setup() { + repository = DefaultConsentRepository( + remoteDataSource = remoteDataSource, + localDataSource = localDataSource + ) + useCase = GetConsentUseCase(repository) + } + + @Test + fun `on get consent successfully for a profile`() { + coEvery { remoteDataSource.getConsent(any()) } returns Result.success( + json.parseToJsonElement(MOCK_JSON_RESPONSE_CONSENT) + ) + runTest(dispatcher) { + val result = useCase.invoke(profileId).first() + assertEquals(ConsentState.ValidState.Granted(ConsentContext.GetConsent), result) + coVerify(exactly = 1) { + repository.getConsent(profileId) + } + } + } + + @Test + fun `on get consent failed for a profile`() { + coEvery { remoteDataSource.getConsent(any()) } returns Result.failure(Throwable("server error")) + runTest(dispatcher) { + val result = useCase.invoke(profileId).first() + assertEquals(ConsentState.ConsentErrorState.Unknown, result) + coVerify(exactly = 1) { + repository.getConsent(profileId) + } + } + } + + companion object { + private const val profileId = "7fo98w-43tgv-23w" + + private val json = Json { + encodeDefaults = true + prettyPrint = false + } + + private val MOCK_JSON_RESPONSE_CONSENT = """{ + "id": "4af9d0b8-7d90-4606-ae3d-12a45a148ff7", + "type": "searchset", + "timestamp": "2023-02-06T08:55:38.043+00:00", + "resourceType": "Bundle", + "total": 0, + "entry": [ + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/Consent/CHARGCONS-X764228532", + "resource": { + "resourceType": "Consent", + "id": "CHARGCONS-X764228532", + "meta": { + "profile": [ + "https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent|1.0" + ] + }, + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "patient-privacy", + "display": "Privacy Consent" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "https://gematik.de/fhir/erpchrg/CodeSystem/GEM_ERPCHRG_CS_ConsentType", + "code": "CHARGCONS", + "display": "Consent for saving electronic charge item" + } + ] + } + ], + "patient": { + "identifier": { + "system": "http://fhir.de/sid/pkv/kvid-10", + "value": "X764228532" + } + }, + "dateTime": "2023-02-03T13:19:04.642+00:00", + "policyRule": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "OPTIN" + } + ] + } + }, + "search": { + "mode": "match" + } + } + ] +} + """.trimIndent() + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GrantConsentUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GrantConsentUseCaseTest.kt index a375efe6..6c8f1219 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GrantConsentUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/GrantConsentUseCaseTest.kt @@ -1,23 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.consent.usecases +import de.gematik.ti.erp.app.consent.model.ConsentContext +import de.gematik.ti.erp.app.consent.model.ConsentState import de.gematik.ti.erp.app.consent.repository.ConsentLocalDataSource import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource import de.gematik.ti.erp.app.consent.repository.ConsentRepository @@ -26,9 +28,9 @@ import de.gematik.ti.erp.app.consent.usecase.GrantConsentUseCase import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -47,12 +49,8 @@ class GrantConsentUseCaseTest { @Before fun setup() { - coEvery { remoteDataSource.getConsent(any()) } returns Result.success( - json.parseToJsonElement( - MOCK_JSON_RESPONSE_CONSENT - ) - ) coEvery { remoteDataSource.grantConsent(any(), any()) } returns Result.success(Unit) + coEvery { localDataSource.getInsuranceId(any()) } returns "123" repository = DefaultConsentRepository( remoteDataSource = remoteDataSource, @@ -68,8 +66,8 @@ class GrantConsentUseCaseTest { @Test fun `on consent granted successfully for a profile`() { runTest(dispatcher) { - val result = useCase.invoke(profileId, insuranceIdentifier) - assertEquals(true, result.isSuccess) + val result = useCase.invoke(profileId).first() + assertEquals(ConsentState.ValidState.Granted(ConsentContext.GrantConsent), result) coVerify(exactly = 1) { repository.grantConsent(profileId, any()) } @@ -80,8 +78,8 @@ class GrantConsentUseCaseTest { fun `on consent granted failed on granting consent for a profile`() { coEvery { remoteDataSource.grantConsent(any(), any()) } returns Result.failure(Throwable("server error")) runTest(dispatcher) { - val result = useCase.invoke(profileId, insuranceIdentifier) - assertEquals(true, result.isFailure) + val result = useCase.invoke(profileId).first() + assertEquals(ConsentState.ConsentErrorState.Unknown, result) coVerify(exactly = 1) { repository.grantConsent(profileId, any()) } @@ -90,73 +88,5 @@ class GrantConsentUseCaseTest { companion object { private const val profileId = "7fo98w-43tgv-23w" - private const val insuranceIdentifier = "867rduzhbfr-wegoug" - - private val json = Json { - encodeDefaults = true - prettyPrint = false - } - - private val MOCK_JSON_RESPONSE_CONSENT = """{ - "id": "4af9d0b8-7d90-4606-ae3d-12a45a148ff7", - "type": "searchset", - "timestamp": "2023-02-06T08:55:38.043+00:00", - "resourceType": "Bundle", - "total": 0, - "entry": [ - { - "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/Consent/CHARGCONS-X764228532", - "resource": { - "resourceType": "Consent", - "id": "CHARGCONS-X764228532", - "meta": { - "profile": [ - "https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent|1.0" - ] - }, - "status": "active", - "scope": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/consentscope", - "code": "patient-privacy", - "display": "Privacy Consent" - } - ] - }, - "category": [ - { - "coding": [ - { - "system": "https://gematik.de/fhir/erpchrg/CodeSystem/GEM_ERPCHRG_CS_ConsentType", - "code": "CHARGCONS", - "display": "Consent for saving electronic charge item" - } - ] - } - ], - "patient": { - "identifier": { - "system": "http://fhir.de/sid/pkv/kvid-10", - "value": "X764228532" - } - }, - "dateTime": "2023-02-03T13:19:04.642+00:00", - "policyRule": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", - "code": "OPTIN" - } - ] - } - }, - "search": { - "mode": "match" - } - } - ] -} - """.trimIndent() } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/RevokeConsentUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/RevokeConsentUseCaseTest.kt new file mode 100644 index 00000000..561984d6 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/RevokeConsentUseCaseTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.usecases + +import de.gematik.ti.erp.app.consent.model.ConsentState +import de.gematik.ti.erp.app.consent.repository.ConsentLocalDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.consent.repository.DefaultConsentRepository +import de.gematik.ti.erp.app.consent.usecase.RevokeConsentUseCase +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class RevokeConsentUseCaseTest { + + private val dispatcher = StandardTestDispatcher() + + private val remoteDataSource = mockk() + + private val localDataSource = mockk() + + private lateinit var repository: ConsentRepository + + private lateinit var useCase: RevokeConsentUseCase + + @Before + fun setup() { + repository = DefaultConsentRepository( + remoteDataSource = remoteDataSource, + localDataSource = localDataSource + ) + useCase = RevokeConsentUseCase(repository) + } + + @Test + fun `on consent revoked successfully for a profile`() { + coEvery { remoteDataSource.deleteChargeConsent(any()) } returns Result.success(Unit) + runTest(dispatcher) { + val result = useCase.invoke("123").first() + assertEquals(ConsentState.ValidState.Revoked, result) + coVerify(exactly = 1) { + repository.revokeChargeConsent("123") + } + } + } + + @Test + fun `on consent revoked failed for a profile`() { + coEvery { remoteDataSource.deleteChargeConsent(any()) } returns Result.failure(Throwable("server error")) + runTest(dispatcher) { + val result = useCase.invoke("123").first() + assertEquals(ConsentState.ConsentErrorState.Unknown, result) + coVerify(exactly = 1) { + repository.revokeChargeConsent("123") + } + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/SaveGrantConsentDrawerShownUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/SaveGrantConsentDrawerShownUseCaseTest.kt new file mode 100644 index 00000000..bc8b22ef --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/consent/usecases/SaveGrantConsentDrawerShownUseCaseTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.consent.usecases + +import de.gematik.ti.erp.app.consent.repository.ConsentLocalDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.consent.repository.DefaultConsentRepository +import de.gematik.ti.erp.app.consent.usecase.SaveGrantConsentDrawerShownUseCase +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SaveGrantConsentDrawerShownUseCaseTest { + + private lateinit var saveGrantConsentDrawerShownUseCase: SaveGrantConsentDrawerShownUseCase + + @MockK(relaxed = true) + private lateinit var localDataSource: ConsentLocalDataSource + + @MockK + private lateinit var remoteDataSource: ConsentRemoteDataSource + + private lateinit var consentRepository: ConsentRepository + + @Before + fun setup() { + MockKAnnotations.init(this) + coEvery { localDataSource.saveGiveConsentDrawerShown("123") } returns Unit + consentRepository = DefaultConsentRepository(remoteDataSource, localDataSource) + saveGrantConsentDrawerShownUseCase = SaveGrantConsentDrawerShownUseCase(consentRepository) + } + + @Test + fun `save consent drawer shown for profileId`() = runTest { + saveGrantConsentDrawerShownUseCase("123") + coVerify(exactly = 1) { + consentRepository.saveGrantConsentDrawerShown("123") + } + coVerify(exactly = 0) { + consentRepository.saveGrantConsentDrawerShown("0") + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt index a18d6028..39661d29 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/SchemaTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/SchemaTest.kt index eceb3e71..05d12a7b 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/SchemaTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/SchemaTest.kt @@ -1,31 +1,33 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("ClassNaming") + package de.gematik.ti.erp.app.db import io.mockk.spyk import io.mockk.verify import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration -import io.realm.kotlin.types.RealmObject import io.realm.kotlin.ext.query +import io.realm.kotlin.types.RealmObject +import org.junit.Test import kotlin.test.assertEquals -import kotlin.test.Test import kotlin.test.assertTrue class RealmA_V1 : RealmObject { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/TestDB.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/TestDB.kt index baac9325..0ad8f122 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/TestDB.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/TestDB.kt @@ -1,42 +1,43 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db +import org.junit.After +import org.junit.Before import java.io.File import java.nio.file.Files import kotlin.io.path.absolutePathString -import kotlin.test.AfterTest -import kotlin.test.BeforeTest private fun createTempDir() = Files.createTempDirectory("realm-db-test").absolutePathString() +@Suppress("UnnecessaryAbstractClass") abstract class TestDB { private lateinit var tempDirPath: String lateinit var tempDBPath: String - @BeforeTest + @Before fun setUpPaths() { tempDirPath = createTempDir() tempDBPath = "$tempDirPath/default" } - @AfterTest + @After fun cleanUpPaths() { File(tempDirPath).deleteRecursively() } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt index 15724e2f..1ebaf223 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtilsTest.kt index c577e739..06b7c542 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/EntityUtilsTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities @@ -28,9 +28,9 @@ import io.realm.kotlin.types.RealmObject import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmListOf import io.realm.kotlin.ext.toRealmList -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test +import org.junit.After +import org.junit.Before +import org.junit.Test import kotlin.test.assertEquals class TestEntryRealm : RealmObject { @@ -72,7 +72,7 @@ class TestRealm : RealmObject, Cascading { class EntityUtilsTest : TestDB() { lateinit var realm: Realm - @BeforeTest + @Before fun setUp() { realm = Realm.open( RealmConfiguration.Builder( @@ -112,7 +112,7 @@ class EntityUtilsTest : TestDB() { } } - @AfterTest + @After fun cleanUp() { realm.close() } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SettingsEntityV1Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SettingsEntityV1Test.kt index 7a6c0acf..4c43f8b6 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SettingsEntityV1Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SettingsEntityV1Test.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.db.entities.v1 @@ -24,8 +24,8 @@ import de.gematik.ti.erp.app.db.queryFirst import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.ext.query +import org.junit.Test -import kotlin.test.Test import kotlin.test.assertEquals class SettingsEntityV1Test : TestDB() { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskDataEntityV1Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskDataEntityV1Test.kt new file mode 100644 index 00000000..00931137 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskDataEntityV1Test.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.db.entities.v1 + +import de.gematik.ti.erp.app.db.TestDB +import de.gematik.ti.erp.app.db.entities.deleteAll +import de.gematik.ti.erp.app.db.entities.v1.invoice.ChargeableItemV1 +import de.gematik.ti.erp.app.db.entities.v1.invoice.InvoiceEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.invoice.PriceComponentV1 +import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.IngredientEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.InsuranceInformationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.MedicationDispenseEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.MedicationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.MedicationRequestEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.MultiplePrescriptionInfoEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.OrganizationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.PatientEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.PractitionerEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.QuantityEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.TaskStatusV1 +import de.gematik.ti.erp.app.db.queryFirst +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.ext.query +import io.realm.kotlin.ext.realmListOf +import org.junit.Test +import kotlin.test.assertEquals + +class SyncedTaskDataEntityV1Test : TestDB() { + @Test + fun `cascading delete`() { + Realm.open( + RealmConfiguration.Builder( + schema = setOf( + SettingsEntityV1::class, + PharmacySearchEntityV1::class, + PasswordEntityV1::class, + TruststoreEntityV1::class, + IdpConfigurationEntityV1::class, + ProfileEntityV1::class, + CommunicationEntityV1::class, + MedicationEntityV1::class, + MedicationDispenseEntityV1::class, + MedicationRequestEntityV1::class, + OrganizationEntityV1::class, + PatientEntityV1::class, + PractitionerEntityV1::class, + ScannedTaskEntityV1::class, + SyncedTaskEntityV1::class, + IdpAuthenticationDataEntityV1::class, + AddressEntityV1::class, + InsuranceInformationEntityV1::class, + ShippingContactEntityV1::class, + IngredientEntityV1::class, + QuantityEntityV1::class, + RatioEntityV1::class, + MultiplePrescriptionInfoEntityV1::class, + PKVInvoiceEntityV1::class, + InvoiceEntityV1::class, + ChargeableItemV1::class, + PriceComponentV1::class + ) + ) + .schemaVersion(0) + .directory(tempDBPath) + .build() + ).also { realm -> + realm.writeBlocking { + copyToRealm( + SyncedTaskEntityV1().apply { + this.taskId = "123" + this.accessCode = "123" + this.lastModified = RealmInstant.MIN + this.expiresOn = RealmInstant.MIN + this.acceptUntil = RealmInstant.MIN + this.authoredOn = RealmInstant.MIN + this.organization = OrganizationEntityV1().apply { + this.address = AddressEntityV1() + } + this.practitioner = PractitionerEntityV1() + this.patient = PatientEntityV1().apply { + this.address = AddressEntityV1() + } + this.insuranceInformation = InsuranceInformationEntityV1() + this.status = TaskStatusV1.Ready + this.medicationRequest = MedicationRequestEntityV1().apply { + this.medication = MedicationEntityV1().apply { + this.amount = RatioEntityV1().apply { + this.numerator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "Tab" + } + this.denominator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "X" + } + } + this.ingredients = realmListOf( + IngredientEntityV1().apply { + this.strength = RatioEntityV1().apply { + this.numerator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "Tab" + } + this.denominator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "X" + } + } + } + ) + } + this.multiplePrescriptionInfo = MultiplePrescriptionInfoEntityV1().apply { + this.indicator = true + this.numbering = RatioEntityV1().apply { + this.denominator = QuantityEntityV1().apply { + this.value = "1" + } + } + } + } + this.medicationDispenses = realmListOf( + MedicationDispenseEntityV1().apply { + this.medication = MedicationEntityV1().apply { + this.amount = RatioEntityV1().apply { + this.numerator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "Tab" + } + this.denominator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "X" + } + } + this.ingredients = realmListOf( + IngredientEntityV1().apply { + this.strength = RatioEntityV1().apply { + this.numerator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "Tab" + } + this.denominator = QuantityEntityV1().apply { + this.value = "1" + this.unit = "X" + } + } + } + ) + } + } + ) + this.communications = realmListOf( + CommunicationEntityV1() + ) + } + ) + } + + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(2, realm.query().count().find()) + assertEquals(2, realm.query().count().find()) + assertEquals(2, realm.query().count().find()) + assertEquals(1, realm.query().count().find()) + assertEquals(5, realm.query().count().find()) + assertEquals(9, realm.query().count().find()) + + realm.writeBlocking { + val syncedTasks = queryFirst()!! + deleteAll(syncedTasks) + } + + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + assertEquals(0, realm.query().count().find()) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskEntityV1Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskEntityV1Test.kt deleted file mode 100644 index a7bc5278..00000000 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/v1/SyncedTaskEntityV1Test.kt +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.db.entities.v1 - -import de.gematik.ti.erp.app.db.TestDB -import de.gematik.ti.erp.app.db.entities.deleteAll -import de.gematik.ti.erp.app.db.entities.v1.invoice.ChargeableItemV1 -import de.gematik.ti.erp.app.db.entities.v1.invoice.InvoiceEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.invoice.PKVInvoiceEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.invoice.PriceComponentV1 -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.IngredientEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.InsuranceInformationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MedicationDispenseEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MedicationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MedicationRequestEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.MultiplePrescriptionInfoEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.OrganizationEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.PatientEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.PractitionerEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.QuantityEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.TaskStatusV1 -import de.gematik.ti.erp.app.db.queryFirst -import io.realm.kotlin.Realm -import io.realm.kotlin.RealmConfiguration -import io.realm.kotlin.types.RealmInstant -import io.realm.kotlin.ext.query -import io.realm.kotlin.ext.realmListOf -import kotlin.test.Test -import kotlin.test.assertEquals - -class SyncedTaskEntityV1Test : TestDB() { - @Test - fun `cascading delete`() { - Realm.open( - RealmConfiguration.Builder( - schema = setOf( - SettingsEntityV1::class, - PharmacySearchEntityV1::class, - PasswordEntityV1::class, - TruststoreEntityV1::class, - IdpConfigurationEntityV1::class, - ProfileEntityV1::class, - CommunicationEntityV1::class, - MedicationEntityV1::class, - MedicationDispenseEntityV1::class, - MedicationRequestEntityV1::class, - OrganizationEntityV1::class, - PatientEntityV1::class, - PractitionerEntityV1::class, - ScannedTaskEntityV1::class, - SyncedTaskEntityV1::class, - IdpAuthenticationDataEntityV1::class, - AddressEntityV1::class, - InsuranceInformationEntityV1::class, - ShippingContactEntityV1::class, - IngredientEntityV1::class, - QuantityEntityV1::class, - RatioEntityV1::class, - MultiplePrescriptionInfoEntityV1::class, - PKVInvoiceEntityV1::class, - InvoiceEntityV1::class, - ChargeableItemV1::class, - PriceComponentV1::class - ) - ) - .schemaVersion(0) - .directory(tempDBPath) - .build() - ).also { realm -> - realm.writeBlocking { - copyToRealm( - SyncedTaskEntityV1().apply { - this.taskId = "123" - this.accessCode = "123" - this.lastModified = RealmInstant.MIN - this.expiresOn = RealmInstant.MIN - this.acceptUntil = RealmInstant.MIN - this.authoredOn = RealmInstant.MIN - this.organization = OrganizationEntityV1().apply { - this.address = AddressEntityV1() - } - this.practitioner = PractitionerEntityV1() - this.patient = PatientEntityV1().apply { - this.address = AddressEntityV1() - } - this.insuranceInformation = InsuranceInformationEntityV1() - this.status = TaskStatusV1.Ready - this.medicationRequest = MedicationRequestEntityV1().apply { - this.medication = MedicationEntityV1().apply { - this.amount = RatioEntityV1().apply { - this.numerator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "Tab" - } - this.denominator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "X" - } - } - this.ingredients = realmListOf( - IngredientEntityV1().apply { - this.strength = RatioEntityV1().apply { - this.numerator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "Tab" - } - this.denominator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "X" - } - } - } - ) - } - this.multiplePrescriptionInfo = MultiplePrescriptionInfoEntityV1().apply { - this.indicator = true - this.numbering = RatioEntityV1().apply { - this.denominator = QuantityEntityV1().apply { - this.value = "1" - } - } - } - } - this.medicationDispenses = realmListOf( - MedicationDispenseEntityV1().apply { - this.medication = MedicationEntityV1().apply { - this.amount = RatioEntityV1().apply { - this.numerator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "Tab" - } - this.denominator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "X" - } - } - this.ingredients = realmListOf( - IngredientEntityV1().apply { - this.strength = RatioEntityV1().apply { - this.numerator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "Tab" - } - this.denominator = QuantityEntityV1().apply { - this.value = "1" - this.unit = "X" - } - } - } - ) - } - } - ) - this.communications = realmListOf( - CommunicationEntityV1() - ) - } - ) - } - - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(2, realm.query().count().find()) - assertEquals(2, realm.query().count().find()) - assertEquals(2, realm.query().count().find()) - assertEquals(1, realm.query().count().find()) - assertEquals(5, realm.query().count().find()) - assertEquals(9, realm.query().count().find()) - - realm.writeBlocking { - val syncedTasks = queryFirst()!! - deleteAll(syncedTasks) - } - - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - assertEquals(0, realm.query().count().find()) - } - } -} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt index a1be7d5d..fc910dcd 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -73,9 +73,10 @@ class CommonRessourceMapperTest { val insuranceInformation = Json.parseToJsonElement(insuranceInformationJson) val result = extractInsuranceInformation( insuranceInformation, - processInsuranceInformation = { name: String?, statusCode: String? -> + processInsuranceInformation = { name, statusCode, coverageType -> assertEquals("HEK", name) assertEquals("3", statusCode) + assertEquals("GKV", coverageType) ReturnType.InsuranceInformation } @@ -123,10 +124,10 @@ class CommonRessourceMapperTest { assertEquals(ReturnType.Quantity, denominator) ReturnType.Ratio }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("Wirkstoff Paulaner Weissbier", text) assertEquals(null, form) - assertEquals("37197", number) + assertEquals(Identifier(ask = "37197"), identifier) assertEquals(null, amount) assertEquals(ReturnType.Ratio, strength) ReturnType.Ingredient @@ -145,10 +146,10 @@ class CommonRessourceMapperTest { ratioFn = { _, _ -> null }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("2-propanol 70 %", text) assertEquals(null, form) - assertEquals(null, number) + assertEquals(Identifier(), identifier) assertEquals("Ad 100 g", amount) assertEquals(null, strength) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt index 891fe82d..b360dfd9 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length") @@ -25,15 +25,16 @@ import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.utils.FhirTemporal import kotlinx.datetime.Instant import kotlinx.serialization.json.Json -import org.junit.Test +import kotlin.test.Test import java.io.File import kotlin.test.assertEquals private const val JsonSymbols = "\"{}[]:" private const val JsonSymbolsEscaped = "\\\"{}[]:" -private val testBundle by lazy { File("$ResourceBasePath/communications_bundle.json").readText() } +// TODO: remove Version 1.2 after 30.Jun.2025 private val testBundleVersion12 by lazy { File("$ResourceBasePath/communications_bundle_version_1_2.json").readText() } +private val testBundleVersion13 by lazy { File("$ResourceBasePath/communications_bundle_version_1_3.json").readText() } class CommunicationMapperTest { @Test @@ -95,30 +96,6 @@ class CommunicationMapperTest { val payload: String? ) - @Suppress("MaxLineLength") - private val communications = mapOf( - 0 to Communication( - taskId = "160.000.000.030.926.11", - communicationId = "01eb8d02-199b-3080-fe9e-ef29caeda984", - orderId = null, - profile = CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.parse("2022-07-06T15:02:03.984+00:00"), - sender = "3-TEST-TID", - recipient = "X110535768", - payload = "{\"version\":\"1\" , \"supplyOptionsType\":\"shipment\" , \"info_text\":\"11 Info\\/Para + HRcode\\/Para + DMC\\/noPara + URL\\/noPara\" , \"pickUpCodeHR\":\"T11__R03\" , \"pickUpCodeDMC\":\"\" , \"url\":\"\"}" - ), - 3 to Communication( - taskId = "160.000.000.030.926.11", - communicationId = "01eb8d01-9a8d-99b8-9277-24b66fb07635", - orderId = null, - profile = CommunicationProfile.ErxCommunicationDispReq, - sentOn = Instant.parse("2022-07-06T14:26:32.387+00:00"), - sender = "X110535768", - recipient = "3-TEST-TID", - payload = "{\"version\":\"1\",\"supplyOptionsType\":\"shipment\",\"name\":\"Prinzessin Lars Graf Freiherr von Schinder\",\"address\":[\"Siegburger Str. 155\",\"\",\"51105 Köln\"],\"hint\":\"\",\"phone\":\"01711111111\"}" - ) - ) - private val communicationsVersion12 = mapOf( 0 to Communication( taskId = "160.000.033.491.280.78", @@ -132,45 +109,77 @@ class CommunicationMapperTest { ) ) - @Test - fun `parse communications`() { - var index = 0 + private val communicationsVersion13 = mapOf( + 0 to Communication( + taskId = "160.000.226.545.733.51", + communicationId = "01ebc980-ae10-41f0-5a9f-c8ad61141a66", + orderId = null, + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.parse("2024-08-14T11:14:38.230Z"), + sender = "3-01.2.2023001.16.103", + recipient = "X110432693", + payload = "Eisern" + ), + 1 to Communication( + taskId = "160.000.226.545.733.51", + communicationId = "01ebc980-c555-9bf8-66b2-0d434e302916", + orderId = null, + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.parse("2024-08-14T11:21:08.651Z"), + sender = "3-01.2.2023001.16.103", + recipient = "X110432693", + payload = "Eisern" + ), + 2 to Communication( + taskId = "160.000.226.545.733.51", + communicationId = "01ebc980-cb72-d730-762e-dd08075f568a", + orderId = null, + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.parse("2024-08-14T11:22:51.230Z"), + sender = "3-01.2.2023001.16.103", + recipient = "X110432693", + payload = "Eisern" + ) + ) + @Test + fun `parse communications version 1_2`() { extractCommunications( - Json.parseToJsonElement(testBundle) + Json.parseToJsonElement(testBundleVersion12) ) { taskId, communicationId, orderId, profile, sentOn, sender, recipient, payload -> - communications[index]?.let { com -> + communicationsVersion12[0]?.let { com -> assertEquals(com.taskId, taskId) assertEquals(com.communicationId, communicationId) assertEquals(com.orderId, orderId) assertEquals(com.profile, profile) - assertEquals(FhirTemporal.Instant(com.sentOn), sentOn) + assertEquals(sentOn.toInstant(), com.sentOn) assertEquals(com.sender, sender) assertEquals(com.recipient, recipient) assertEquals(com.payload, payload) } - - index++ } - - assertEquals(15, index) } @Test - fun `parse communications version 1_2`() { + fun `parse communications version 1_3`() { + var index = 0 + extractCommunications( - Json.parseToJsonElement(testBundleVersion12) + Json.parseToJsonElement(testBundleVersion13) ) { taskId, communicationId, orderId, profile, sentOn, sender, recipient, payload -> - communicationsVersion12[0]?.let { com -> + communicationsVersion13[index]?.let { com -> assertEquals(com.taskId, taskId) assertEquals(com.communicationId, communicationId) assertEquals(com.orderId, orderId) assertEquals(com.profile, profile) - assertEquals(sentOn.toInstant(), com.sentOn) + assertEquals(FhirTemporal.Instant(com.sentOn), sentOn) assertEquals(com.sender, sender) assertEquals(com.recipient, recipient) assertEquals(com.payload, payload) } + index++ } + + assertEquals(3, index) } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapperTest.kt index c238fabe..e2d2df33 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/InvoiceMapperTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt index 7bd898a7..5b248c17 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -24,8 +24,9 @@ import de.gematik.ti.erp.app.utils.asFhirTemporal import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json -import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.Test +import kotlin.test.assertTrue class MedicationDispenseMapperTest { @@ -42,19 +43,18 @@ class MedicationDispenseMapperTest { assertEquals(ReturnType.Quantity, denominator) ReturnType.Ratio }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("Wirkstoff Paulaner Weissbier", text) assertEquals(null, form) - assertEquals("", number) assertEquals(null, amount) + assertEquals(Identifier(), identifier) assertEquals(ReturnType.Ratio, strength) ReturnType.Ingredient }, - processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + processMedication = { text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals("Defamipin", text) - assertEquals(MedicationProfile.PZN, medicationProfile) assertEquals(MedicationCategory.BTM, medicationCategory) assertEquals("FET", form) assertEquals(ReturnType.Ratio, amount) @@ -62,7 +62,7 @@ class MedicationDispenseMapperTest { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("Sonstiges", normSizeCode) - assertEquals("06491772", uniqueIdentifier) + assertEquals("06491772", identifier.pzn) assertEquals(listOf(), ingredients) assertEquals("8521037577", lotNumber) assertEquals(FhirTemporal.Instant(Instant.parse("2023-05-02T06:26:06Z")), expirationDate) @@ -99,11 +99,10 @@ class MedicationDispenseMapperTest { ingredientFn = { _, _, _, _, _ -> ReturnType.Ingredient }, - processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + processMedication = { text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals("", text) - assertEquals(MedicationProfile.UNKNOWN, medicationProfile) assertEquals(MedicationCategory.UNKNOWN, medicationCategory) assertEquals(null, form) assertEquals(null, amount) @@ -111,7 +110,7 @@ class MedicationDispenseMapperTest { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), identifier) assertEquals(listOf(), ingredients) assertEquals(null, lotNumber) assertEquals(null, expirationDate) @@ -149,11 +148,10 @@ class MedicationDispenseMapperTest { ingredientFn = { _, _, _, _, _ -> ReturnType.Ingredient }, - processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + processMedication = { text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals("Sumatriptan-1a Pharma 100 mg Tabletten", text) - assertEquals(MedicationProfile.PZN, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("TAB", form) assertEquals(ReturnType.Ratio, amount) @@ -161,7 +159,7 @@ class MedicationDispenseMapperTest { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("N1", normSizeCode) - assertEquals("06313728", uniqueIdentifier) + assertEquals("06313728", identifier.pzn) assertEquals(listOf(), ingredients) assertEquals(null, lotNumber) assertEquals(null, expirationDate) @@ -196,19 +194,18 @@ class MedicationDispenseMapperTest { assertEquals(ReturnType.Quantity, denominator) ReturnType.Ratio }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("Wirkstoff Paulaner Weissbier", text) assertEquals(null, form) - assertEquals("", number) + assertEquals(Identifier(), identifier) assertEquals(null, amount) assertEquals(ReturnType.Ratio, strength) ReturnType.Ingredient }, - processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + processMedication = { text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals("Defamipin", text) - assertEquals(MedicationProfile.PZN, medicationProfile) assertEquals(MedicationCategory.UNKNOWN, medicationCategory) assertEquals("FET", form) assertEquals(ReturnType.Ratio, amount) @@ -216,7 +213,7 @@ class MedicationDispenseMapperTest { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("Sonstiges", normSizeCode) - assertEquals("06491772", uniqueIdentifier) + assertEquals("06491772", identifier.pzn) assertEquals(listOf(), ingredients) assertEquals("8521037577", lotNumber) assertEquals(Instant.parse("2023-05-02T06:26:06Z"), expirationDate?.toInstant()) @@ -236,4 +233,151 @@ class MedicationDispenseMapperTest { ) assertEquals(ReturnType.MedicationDispense, result) } + + @Test + fun `extract medication dispense simple medication version 1_4`() { + val medicationDispensesBundle = Json.parseToJsonElement(dispenseSimpleMedication_1_4) + val dispensesWithMedication = extractMedicationDispensePairs(medicationDispensesBundle) + + assertEquals(1, dispensesWithMedication.size) + + dispensesWithMedication.forEach { (dispense, dispenseMedication) -> + val result = extractMedicationDispenseWithMedication( + dispense, + dispenseMedication, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, ingredientMedications, + ingredients, lotNumber, expirationDate -> + assertEquals("06313728", text) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals(null, form) + assertEquals(null, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals(null, normSizeCode) + assertEquals("06313728", identifier.pzn) + assertEquals(listOf(), ingredients) + assertTrue(ingredientMedications.isEmpty()) + assertEquals(null, lotNumber) + assertEquals(null, expirationDate) + ReturnType.Medication + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, + dosageInstruction, performer, whenHandedOver -> + assertEquals("160.000.000.000.000.01", dispenseId) + assertEquals("X123456789", patientIdentifier) + assertEquals(ReturnType.Medication, medication) + assertEquals(false, wasSubstituted) + assertEquals(null, dosageInstruction) + assertEquals("3-SMC-B-Testkarte-883110000095957", performer) + assertEquals(LocalDate.parse("2025-09-06").asFhirTemporal(), whenHandedOver) + ReturnType.MedicationDispense + } + ) + assertEquals(ReturnType.MedicationDispense, result) + } + } + + @Test + fun `extract medication dispense compounding medication version 1_4`() { + val medicationDispensesBundle = Json.parseToJsonElement(dispenseCompoundingMedication_1_4) + val dispensesWithMedication = extractMedicationDispensePairs(medicationDispensesBundle) + + assertEquals(1, dispensesWithMedication.size) + + dispensesWithMedication.forEach { (dispense, dispenseMedication) -> + val result = extractMedicationDispenseWithMedication( + dispense, + dispenseMedication, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { _, _, _, _, _, + _, _, _, _, _, + _, _, _ -> + ReturnType.Medication + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, + dosageInstruction, performer, whenHandedOver -> + assertEquals("160.000.000.000.000.03", dispenseId) + assertEquals("X123456789", patientIdentifier) + assertEquals(ReturnType.Medication, medication) + assertEquals(false, wasSubstituted) + assertEquals(null, dosageInstruction) + assertEquals("3-SMC-B-Testkarte-883110000095957", performer) + assertEquals(LocalDate.parse("2025-09-06").asFhirTemporal(), whenHandedOver) + ReturnType.MedicationDispense + } + ) + assertEquals(ReturnType.MedicationDispense, result) + } + } + + @Test + fun `extract multiple dispenses with medications version 1_4`() { + val medicationDispensesBundle = Json.parseToJsonElement(dispenseMultipleMedication_1_4) + val dispensesWithMedication = extractMedicationDispensePairs(medicationDispensesBundle) + + assertEquals(2, dispensesWithMedication.size) + + dispensesWithMedication.forEachIndexed() { index, (dispense, dispenseMedication) -> + val result = extractMedicationDispenseWithMedication( + dispense, + dispenseMedication, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { _, _, _, _, _, + _, _, _, _, _, + _, _, _ -> + ReturnType.Medication + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, + dosageInstruction, performer, whenHandedOver -> + + if (index == 0) { + assertEquals("160.000.000.000.000.01", dispenseId) + } else { + assertEquals("160.000.000.000.000.02", dispenseId) + } + assertEquals("X123456789", patientIdentifier) + assertEquals(ReturnType.Medication, medication) + assertEquals(false, wasSubstituted) + assertEquals(null, dosageInstruction) + assertEquals("3-SMC-B-Testkarte-883110000095957", performer) + assertEquals(LocalDate.parse("2025-09-06").asFhirTemporal(), whenHandedOver) + ReturnType.MedicationDispense + } + ) + assertEquals(ReturnType.MedicationDispense, result) + } + } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapperTest.kt index fcc9ff31..83bb2764 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapperTest.kt @@ -1,28 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model import kotlinx.datetime.LocalTime import kotlinx.serialization.json.Json +import org.junit.Test import java.io.File import java.time.DayOfWeek -import kotlin.test.Test import kotlin.test.assertEquals private val testBundle by lazy { File("$ResourceBasePath/pharmacy_result_bundle.json").readText() } @@ -43,7 +43,7 @@ class PharmacyMapperTest { postalCode = "27578", city = "Bremerhaven" ), - location = Location(latitude = 8.597412, longitude = 53.590027), + coordinates = Coordinates(latitude = 8.597412, longitude = 53.590027), contacts = PharmacyContacts( phone = "0471/87029", mail = "info@heide-apotheke-bremerhaven.de", @@ -53,7 +53,7 @@ class PharmacyMapperTest { onlineServiceUrl = "" ), provides = listOf( - LocalPharmacyService( + PharmacyService.LocalPharmacyService( name = "Heide-Apotheke", openingHours = OpeningHours( openingTime = mapOf( @@ -66,7 +66,7 @@ class PharmacyMapperTest { ) ) ), - DeliveryPharmacyService( + PharmacyService.DeliveryPharmacyService( name = "Heide-Apotheke", openingHours = OpeningHours( openingTime = mapOf( @@ -78,10 +78,10 @@ class PharmacyMapperTest { ) ) ), - OnlinePharmacyService( + PharmacyService.OnlinePharmacyService( name = "Heide-Apotheke" ), - PickUpPharmacyService( + PharmacyService.PickUpPharmacyService( name = "Heide-Apotheke" ) ), @@ -97,7 +97,7 @@ class PharmacyMapperTest { postalCode = "82139", city = "Starnberg" ), - location = Location(latitude = 48.0018513, longitude = 11.3497755), + coordinates = Coordinates(latitude = 48.0018513, longitude = 11.3497755), contacts = PharmacyContacts( phone = "", mail = "", @@ -109,12 +109,12 @@ class PharmacyMapperTest { onlineServiceUrl = "" ), provides = listOf( - LocalPharmacyService( + PharmacyService.LocalPharmacyService( name = "PT-STA-Apotheke 2TEST-ONLY", openingHours = OpeningHours(mapOf()) ), - OnlinePharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY"), - PickUpPharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY") + PharmacyService.OnlinePharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY"), + PharmacyService.PickUpPharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY") ), telematikId = "3-SMC-B-Testkarte-883110000116948" ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperEpaVersion_1_4_Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperEpaVersion_1_4_Test.kt new file mode 100644 index 00000000..15931b7f --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperEpaVersion_1_4_Test.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.fhir.parser.filterWith +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.utils.toFhirTemporal +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@Suppress("ClassNaming") +class RessourceMapperEpaVersion_1_4_Test { + + @Test + fun `process simple medication version 1_4`() { + val medicationJson = Json.parseToJsonElement(simpleMedication_1_4) + extractDispenseMedication( + medicationJson, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + ingredientMedication, + ingredients, + lotNumber, + expirationDate -> + assertEquals("82082973", text) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals(null, form) + assertEquals(null, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals(null, normSizeCode) + assertEquals(Identifier("82082973"), identifier) + assertEquals(listOf(), ingredients) + assertTrue(ingredientMedication.isEmpty()) + + assertEquals("1419556306", lotNumber) + assertEquals("2024-12-25T02:35:18+00:00".toFhirTemporal(), expirationDate) + + ReturnType.Medication + } + ) + } + + @Test + fun `process pharmaceutical product`() { + val medicationJson = Json.parseToJsonElement(pharmaceuticalProduct) + extractDispenseMedication( + medicationJson, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + ingredientMedication, + ingredients, + lotNumber, + expirationDate -> + assertEquals("01746517-1", text) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals(null, form) + assertEquals(null, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals(null, normSizeCode) + assertEquals(Identifier(), identifier) + assertEquals(1, ingredients.size) + assertTrue(ingredientMedication.isEmpty()) + + assertEquals(null, lotNumber) + assertEquals(null, expirationDate) + } + ) + val ingredient = medicationJson.findAll("ingredient").toList()[0] + + ingredient.extractEpaIngredient( + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { text, + form, + identifier, + amount, + strength -> + + assertEquals("Natriumcromoglicat", text) + assertEquals(null, form) + assertEquals(Identifier(atc = "R01AC01"), identifier) + assertEquals(null, amount) + assertEquals(ReturnType.Ratio, strength) + ReturnType.Ingredient + } + ) + } + + @Test + fun `process complex medication version 1_4`() { + val medicationJson = Json.parseToJsonElement(complexMedication_1_4) + extractDispenseMedication( + medicationJson, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _, + _ -> + + ReturnType.Medication + } + ) + } + + @Test + fun `process contained medication version 1_4`() { + val medicationJson = Json.parseToJsonElement(complexMedication_1_4) + + val containedMedications = medicationJson.findAll("contained") + .filterWith( + "resourceType", + stringValue("Medication") + ).toList() + + extractContainedMedication( + containedMedications[0], + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processIngredientMedication = { text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + ingredientMedication, + ingredients, + lotNumber, + expirationDate -> + + assertEquals(null, text) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals(null, form) + assertEquals(null, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals(null, normSizeCode) + assertEquals(Identifier(), identifier) + assertTrue(ingredientMedication.isEmpty()) + assertTrue(ingredients.isNotEmpty()) + assertEquals(null, lotNumber) + assertEquals(null, expirationDate) + } + ) + + extractContainedMedication( + containedMedications[1], + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processIngredientMedication = { text, + medicationCategory, + form, + amount, + vaccine, + manufacturingInstructions, + packaging, + normSizeCode, + identifier, + ingredientMedication, + ingredients, + lotNumber, + expirationDate -> + + assertEquals(null, text) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals(null, form) + assertEquals(null, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals(null, normSizeCode) + assertEquals(Identifier(), identifier) + assertTrue(ingredientMedication.isEmpty()) + assertTrue(ingredients.isNotEmpty()) + assertEquals(null, lotNumber) + assertEquals(null, expirationDate) + } + + ) + + val ingredientsFromFirstContained = containedMedications[0].findAll("ingredient").toList() + assertEquals(1, ingredientsFromFirstContained.size) + + val ingredientsFromSecondContained = containedMedications[1].findAll("ingredient").toList() + assertEquals(1, ingredientsFromSecondContained.size) + + ingredientsFromFirstContained[0].extractEpaIngredient( + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { text, form, identifier, amount, strength -> + assertEquals("Natriumcromoglicat", text) + assertEquals(null, form) + assertEquals(Identifier(atc = "R01AC01"), identifier) + assertEquals(null, amount) + assertEquals(ReturnType.Ratio, strength) + } + ) + + ingredientsFromSecondContained[0].extractEpaIngredient( + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { text, form, identifier, amount, strength -> + assertEquals("Natriumcromoglicat", text) + assertEquals(null, form) + assertEquals(Identifier(atc = "R01AC01"), identifier) + assertEquals(null, amount) + assertEquals(ReturnType.Ratio, strength) + } + ) + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt index f9eac6ae..27b53fd0 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -98,11 +98,10 @@ class RessourceMapperVersion102Test { ReturnType.Ratio }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, ingredientMedication, ingredients, lotnumber, expirationDate -> assertEquals("Ich bin in Einlösung", text) - assertEquals(MedicationProfile.PZN, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("IHP", form) assertEquals(ReturnType.Ratio, amount) @@ -110,7 +109,7 @@ class RessourceMapperVersion102Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("N1", normSizeCode) - assertEquals("00427833", uniqueIdentifier) + assertEquals("00427833", uniqueIdentifier.pzn) assertEquals(listOf(), ingredients) assertEquals(null, lotnumber) assertEquals(null, expirationDate) @@ -133,20 +132,20 @@ class RessourceMapperVersion102Test { assertEquals(ReturnType.Quantity, denominator) ReturnType.Ratio }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("Wirkstoff Paulaner Weissbier", text) assertEquals(null, form) assertEquals(null, amount) - assertEquals("37197", number) + assertEquals(Identifier(ask = "37197"), identifier) assertEquals(ReturnType.Ratio, strength) ReturnType.Ingredient }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, + _, ingredients, lotNumber, expirationDate -> assertEquals(null, text) - assertEquals(MedicationProfile.INGREDIENT, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("Flüssigkeiten", form) assertEquals(null, amount) @@ -154,7 +153,7 @@ class RessourceMapperVersion102Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("N1", normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), identifier) assertEquals(listOf(ReturnType.Ingredient), ingredients) assertEquals(null, lotNumber) @@ -181,11 +180,10 @@ class RessourceMapperVersion102Test { ReturnType.Ingredient }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, + text, medicationCategory, form, amount, vaccine, manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotNumber, expirationDate -> + ingredientMedication, ingredients, lotNumber, expirationDate -> assertEquals(null, text) - assertEquals(MedicationProfile.COMPOUNDING, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("Lösung", form) assertEquals(ReturnType.Ratio, amount) @@ -193,7 +191,7 @@ class RessourceMapperVersion102Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), uniqueIdentifier) assertEquals(listOf(ReturnType.Ingredient, ReturnType.Ingredient), ingredients) assertEquals(null, lotNumber) @@ -216,11 +214,10 @@ class RessourceMapperVersion102Test { ReturnType.Ratio }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, + text, medicationCategory, form, amount, vaccine, manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotNumber, expirationDate -> + ingredientMedication, ingredients, lotNumber, expirationDate -> assertEquals("Freitext", text) - assertEquals(MedicationProfile.FREETEXT, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals(null, form) assertEquals(null, amount) @@ -228,9 +225,8 @@ class RessourceMapperVersion102Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), uniqueIdentifier) assertEquals(listOf(), ingredients) - assertEquals(null, lotNumber) assertEquals(null, expirationDate) ReturnType.Medication diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt index 4d3a347f..2c8434f0 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -112,11 +112,10 @@ class RessourceMapperVersion110Test { ReturnType.Ratio }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, + text, medicationCategory, form, amount, vaccine, manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotnumber, expirationDate -> + ingredientMedications, ingredients, lotnumber, expirationDate -> assertEquals("Novaminsulfon 500 mg Lichtenstein 100 ml Tropf. N3", text) - assertEquals(MedicationProfile.PZN, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("TEI", form) assertEquals(ReturnType.Ratio, amount) @@ -124,7 +123,7 @@ class RessourceMapperVersion110Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals("03507952", uniqueIdentifier) + assertEquals("03507952", uniqueIdentifier.pzn) assertEquals(listOf(), ingredients) assertEquals(null, lotnumber) assertEquals(null, expirationDate) @@ -147,20 +146,19 @@ class RessourceMapperVersion110Test { assertEquals(ReturnType.Quantity, denominator) ReturnType.Ratio }, - ingredientFn = { text, form, number, amount, strength -> + ingredientFn = { text, form, identifier, amount, strength -> assertEquals("Ramipril", text) assertEquals(null, form) assertEquals(null, amount) - assertEquals("22686", number) + assertEquals(Identifier(ask = "22686"), identifier) assertEquals(ReturnType.Ratio, strength) ReturnType.Ingredient }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotNumber, expirationDate -> + text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, + ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals(null, text) - assertEquals(MedicationProfile.INGREDIENT, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("Tabletten", form) assertEquals(ReturnType.Ratio, amount) @@ -168,7 +166,7 @@ class RessourceMapperVersion110Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals("N3", normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), identifier) assertEquals(listOf(ReturnType.Ingredient), ingredients) assertEquals(null, lotNumber) @@ -195,11 +193,10 @@ class RessourceMapperVersion110Test { ReturnType.Ingredient }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotNumber, expirationDate -> + text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, + ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals(null, text) - assertEquals(MedicationProfile.COMPOUNDING, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals("Kapseln", form) assertEquals(ReturnType.Ratio, amount) @@ -207,7 +204,7 @@ class RessourceMapperVersion110Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), identifier) assertEquals(listOf(ReturnType.Ingredient, ReturnType.Ingredient, ReturnType.Ingredient), ingredients) assertEquals(null, lotNumber) @@ -230,11 +227,10 @@ class RessourceMapperVersion110Test { ReturnType.Ratio }, processMedication = { - text, medicationProfile, medicationCategory, form, amount, vaccine, - manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, - ingredients, lotNumber, expirationDate -> + text, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, identifier, + ingredientMedications, ingredients, lotNumber, expirationDate -> assertEquals("Metformin 850mg Tabletten N3", text) - assertEquals(MedicationProfile.FREETEXT, medicationProfile) assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) assertEquals(null, form) assertEquals(null, amount) @@ -242,7 +238,7 @@ class RessourceMapperVersion110Test { assertEquals(null, manufacturingInstructions) assertEquals(null, packaging) assertEquals(null, normSizeCode) - assertEquals(null, uniqueIdentifier) + assertEquals(Identifier(), identifier) assertEquals(listOf(), ingredients) assertEquals(null, lotNumber) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskDataMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskDataMapperTest.kt new file mode 100644 index 00000000..c8cb1561 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskDataMapperTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.utils.asFhirTemporal +import de.gematik.ti.erp.app.utils.toFhirTemporal +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals + +class TaskMapperTest { + class TaskDataMapperTest { + + @Test + fun `extract task version 1_2`() { + val taskJson = Json.parseToJsonElement(taskJson_vers_1_2) + extractTask( + taskJson, + process = { taskId, accessCode, lastModified, expiresOn, + acceptUntil, authoredOn, status, lastMedicationDispense -> + + assertEquals("160.000.033.491.280.78", taskId) + assertEquals("777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea", accessCode) + assertEquals(Instant.parse("2022-03-18T15:29:00Z"), lastModified.toInstant()) + assertEquals(LocalDate.parse("2022-06-02").asFhirTemporal(), expiresOn) + assertEquals(LocalDate.parse("2022-04-02").asFhirTemporal(), acceptUntil) + assertEquals(Instant.parse("2022-03-18T15:26:00Z"), authoredOn.toInstant()) + assertEquals(TaskStatus.Completed, status) + assertEquals(null, lastMedicationDispense) + } + ) + } + + @Test + fun `extract task version 1_3`() { + val taskJson = Json.parseToJsonElement(taskJson_vers_1_3) + extractTask( + taskJson, + process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, + authoredOn, status, lastMedicationDispense -> + assertEquals("160.123.456.789.123.61", taskId) + assertEquals("777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607bl", accessCode) + assertEquals(Instant.parse("2020-03-02T08:45:05+00:00"), lastModified.toInstant()) + assertEquals(LocalDate.parse("2020-06-02").asFhirTemporal(), expiresOn) + assertEquals(LocalDate.parse("2020-04-01").asFhirTemporal(), acceptUntil) + assertEquals(Instant.parse("2020-03-02T08:25:05+00:00"), authoredOn.toInstant()) + assertEquals(TaskStatus.InProgress, status) + assertEquals(Instant.parse("2020-04-01T15:37:17Z"), lastMedicationDispense?.toInstant()) + } + ) + } + + @Test + fun `extract task id and current status from bundle version 1_2`() { + val taskJson = Json.parseToJsonElement(task_bundle_version_1_2) + val (bundleTotal, taskdata) = extractActualTaskData(taskJson) + assertEquals(1, bundleTotal) + + val taskId = taskdata[0].taskId + val status = taskdata[0].status + val lastModified = taskdata[0].lastModified + + assertEquals("160.000.033.491.280.78", taskId) + assertEquals(TaskStatus.Ready, status) + assertEquals("2022-03-18T15:27:00Z".toFhirTemporal(), lastModified) + } + + @Test + fun `extract task id's and current status version 1_3`() { + val taskJson = Json.parseToJsonElement(task_bundle_version_1_3) + val (bundleTotal, taskdata) = extractActualTaskData(taskJson) + assertEquals(3, bundleTotal) + assertEquals("160.123.456.789.123.58", taskdata[0].taskId) + assertEquals("160.123.456.789.123.78", taskdata[1].taskId) + assertEquals("160.123.456.789.123.61", taskdata[2].taskId) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt deleted file mode 100644 index 16444895..00000000 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.fhir.model - -import de.gematik.ti.erp.app.utils.asFhirTemporal -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDate -import kotlinx.serialization.json.Json -import org.junit.Test -import kotlin.test.assertEquals - -class TaskMapperTest { - - @Test - fun `extract task version 1_1_1`() { - val taskJson = Json.parseToJsonElement(taskJson_vers_1_1_1) - extractTask( - taskJson, - process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> - assertEquals("160.000.000.029.982.30", taskId) - assertEquals("dd23212d35d14ccde351f9a1077f3d9508dcb8629882627ec16a22ea86144290", accessCode) - assertEquals(Instant.parse("2022-06-09T11:57:37.923Z"), lastModified.toInstant()) - assertEquals(LocalDate.parse("2022-09-09").asFhirTemporal(), expiresOn) - assertEquals(LocalDate.parse("2022-07-07").asFhirTemporal(), acceptUntil) - assertEquals(Instant.parse("2022-06-09T11:50:23.223Z"), authoredOn.toInstant()) - assertEquals(TaskStatus.Completed, status) - } - ) - } - - @Test - fun `extract task version 1_2`() { - val taskJson = Json.parseToJsonElement(taskJson_vers_1_2) - extractTask( - taskJson, - process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> - assertEquals("160.000.033.491.280.78", taskId) - assertEquals("777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea", accessCode) - assertEquals(Instant.parse("2022-03-18T15:29:00Z"), lastModified.toInstant()) - assertEquals(LocalDate.parse("2022-06-02").asFhirTemporal(), expiresOn) - assertEquals(LocalDate.parse("2022-04-02").asFhirTemporal(), acceptUntil) - assertEquals(Instant.parse("2022-03-18T15:26:00Z"), authoredOn.toInstant()) - assertEquals(TaskStatus.Completed, status) - } - ) - } - - @Test - fun `extract task id from bundle`() { - val taskJson = Json.parseToJsonElement(task_bundle_version_1_2) - val (bundleTotal, taskIds) = extractTaskIds(taskJson) - assertEquals(1, bundleTotal) - assertEquals("160.000.033.491.280.78", taskIds[0]) - } -} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt index 532cef54..2cad4a1b 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.model @@ -22,12 +22,12 @@ import java.io.File const val ResourceBasePath = "src/commonTest/resources/" -val taskJson_vers_1_1_1 by lazy { - File("$ResourceBasePath/fhir/task.json").readText() -} val taskJson_vers_1_2 by lazy { File("$ResourceBasePath/fhir/task_vers_1_2.json").readText() } +val taskJson_vers_1_3 by lazy { + File("$ResourceBasePath/fhir/task_vers_1_3.json").readText() +} val organizationJson by lazy { File("$ResourceBasePath/fhir/organization.json").readText() @@ -124,9 +124,40 @@ val medDispenseBundleVersion_1_2 by lazy { File("$ResourceBasePath/fhir/bundle_med_dispense_version_1_2.json").readText() } +val dispenseSimpleMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/bundle_dispense_simple_medication.json").readText() +} + +val simpleMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/resource_simple_medication.json").readText() +} + +val pharmaceuticalProduct by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/pharmaceuticalProduct.json").readText() +} + +val complexMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/Medication-Medication-Kombipackung.json").readText() +} + +val complexIngredientMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/Medication-Medication-Rezeptur.json").readText() +} + +val dispenseMultipleMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/bundle_multiple_dispenses_simple_medications.json").readText() +} + +val dispenseCompoundingMedication_1_4 by lazy { + File("$ResourceBasePath/fhir/workflow_version_1_4/bundle_dispense_compounding_medication.json").readText() +} + val task_bundle_version_1_2 by lazy { File("$ResourceBasePath/fhir/task_bundle_vers_1_2.json").readText() } +val task_bundle_version_1_3 by lazy { + File("$ResourceBasePath/fhir/task_bundle_vers_1_3.json").readText() +} val charge_item_bundle_version_1_2 by lazy { File("$ResourceBasePath/fhir/pkv/charge_item_bundle_vers_1_2.json").readText() diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ComparatorTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ComparatorTest.kt index b4a8d904..b5244844 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ComparatorTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ComparatorTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt index 98835b28..3a0995e2 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/FormatterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/FormatterTest.kt index f5cf70f9..dcdc8bdf 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/FormatterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/FormatterTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ParserTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ParserTest.kt index e8ab9bb9..61bc9c02 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ParserTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ParserTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt index b3fc18b7..a1c7a917 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TestData.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TestData.kt index 73c281d6..76e1cf73 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TestData.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TestData.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.fhir.parser diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicDataTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicDataTest.kt index abef258f..4332383f 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicDataTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicDataTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.api.models diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt index 398a92da..81d14646 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.repository @@ -55,7 +55,6 @@ import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import io.mockk.MockKAnnotations -import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk @@ -65,11 +64,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder import org.jose4j.base64url.Base64 -import org.jose4j.jws.JsonWebSignature -import org.jose4j.jwx.JsonWebStructure import org.junit.Before import org.junit.Rule import org.junit.Test @@ -78,8 +74,8 @@ import java.security.Security import kotlin.test.assertEquals import kotlin.time.Duration.Companion.minutes -const val EXPECTED_EXPIRATION_TIME = 1616143876L -const val EXPECTED_ISSUE_TIME = 1616057476L +// const val EXPECTED_EXPIRATION_TIME = 1616143876L +// const val EXPECTED_ISSUE_TIME = 1616057476L class CommonIdpRepositoryTest : TestDB() { @@ -99,17 +95,19 @@ class CommonIdpRepositoryTest : TestDB() { lateinit var realm: Realm private lateinit var repo: IdpRepository - private val testDiscoveryDocument by lazy { File("$ResourceBasePath/idp/discovery-doc.jwt").readText() } - private val testCertificateDocument by lazy { File("$ResourceBasePath/idp/idpCertificate.txt").readText() } + + // private val testDiscoveryDocument by lazy { File("$ResourceBasePath/idp/discovery-doc.jwt").readText() } + // private val testCertificateDocument by lazy { File("$ResourceBasePath/idp/idpCertificate.txt").readText() } + private val ssoToken by lazy { File("$ResourceBasePath/idp/sso-token.txt").readText() } private val healthCardCert = X509CertificateHolder( Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) ) // private val healthCardCertPrivateKey = BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY - private val x509Certificate = X509CertificateHolder(Base64.decode(testCertificateDocument)) + // private val x509Certificate = X509CertificateHolder(Base64.decode(testCertificateDocument)) - private val testIdpConfig = IdpData.IdpConfiguration( + /*private val testIdpConfig = IdpData.IdpConfiguration( authorizationEndpoint = "http://localhost:8888/sign_response", ssoEndpoint = "http://localhost:8888/sso_response", tokenEndpoint = "http://localhost:8888/token", @@ -124,7 +122,7 @@ class CommonIdpRepositoryTest : TestDB() { federationAuthorizationIDsEndpoint = "", // not found in test data thirdPartyAuthorizationEndpoint = "http://localhost:8888/thirdPartyAuth", federationAuthorizationEndpoint = "" // not found in test data - ) + )*/ @MockK lateinit var remoteDataSource: IdpRemoteDataSource @@ -178,7 +176,7 @@ class CommonIdpRepositoryTest : TestDB() { idpLocalDataSource = IdpLocalDataSource(realm) - repo = IdpRepository( + repo = DefaultIdpRepository( remoteDataSource = remoteDataSource, localDataSource = idpLocalDataSource, accessTokenDataSource = accessTokenDataSource @@ -222,7 +220,7 @@ class CommonIdpRepositoryTest : TestDB() { assertEquals(ssoToken, savedSsoToken) } - @Test + /*@Test fun `load unchecked idp configuration`() { val discoveryDocument = JWSDiscoveryDocument( JsonWebStructure.fromCompactSerialization( @@ -256,5 +254,5 @@ class CommonIdpRepositoryTest : TestDB() { val savedIdpConfig = idpLocalDataSource.loadIdpInfo() assertEquals(testIdpConfig, savedIdpConfig) } - } + }*/ } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt index dca55250..b09dbe1a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase @@ -81,6 +82,12 @@ class IdpBasicUseCaseTest { coEvery { truststoreUseCase.checkIdpCertificate(any(), any()) } coAnswers {} } + @Requirement( + "A_20512#3", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Testing the implementation.", + codeLines = 6 + ) @Test fun `checkIdpConfigurationValidity - valid document`() = runTest { useCase.checkIdpConfigurationValidity( @@ -89,6 +96,12 @@ class IdpBasicUseCaseTest { ) } + @Requirement( + "A_20512#4", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Testing the implementation.", + codeLines = 8 + ) @Test(expected = Exception::class) fun `checkIdpConfigurationValidity - document expired - throws exception`() = runTest { useCase.checkIdpConfigurationValidity( diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt index 3af1f0d7..5c424a6a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.idp.usecase @@ -26,10 +26,10 @@ import de.gematik.ti.erp.app.idp.api.IdpService import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.AccessTokenDataSource +import de.gematik.ti.erp.app.idp.repository.DefaultIdpRepository import de.gematik.ti.erp.app.idp.repository.IdpLocalDataSource import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository import de.gematik.ti.erp.app.idp.repository.IdpRemoteDataSource -import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase import io.mockk.MockKAnnotations @@ -83,7 +83,7 @@ class IdpIntegrationTest { private val lock: Mutex = mockk(relaxed = true) - private lateinit var idpRepository: IdpRepository + private lateinit var idpRepository: DefaultIdpRepository private lateinit var idpPairingRepository: IdpPairingRepository private lateinit var basicUseCase: IdpBasicUseCase private lateinit var useCase: IdpUseCase @@ -128,7 +128,7 @@ class IdpIntegrationTest { .create(IdpService::class.java) idpRepository = spyk( - IdpRepository( + DefaultIdpRepository( remoteDataSource = IdpRemoteDataSource(idpService) { BuildKonfig.IDP_DEFAULT_SCOPE }, localDataSource = localDataSource, accessTokenDataSource = accessTokenDataSource diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt index febc181f..c67d02f0 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.invoice.repository @@ -59,10 +59,11 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import kotlinx.serialization.json.Json -import org.junit.Rule import org.junit.Before +import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals + class InvoiceRepositoryTest : TestDB() { @get:Rule @@ -70,7 +71,7 @@ class InvoiceRepositoryTest : TestDB() { lateinit var invoiceLocalDataSource: InvoiceLocalDataSource lateinit var invoiceRemoteDataSource: InvoiceRemoteDataSource - lateinit var invoiceRepository: InvoiceRepository + lateinit var invoiceRepository: DefaultInvoiceRepository lateinit var profileRepository: ProfileRepository lateinit var realm: Realm @@ -120,7 +121,7 @@ class InvoiceRepositoryTest : TestDB() { invoiceLocalDataSource = InvoiceLocalDataSource(realm) invoiceRemoteDataSource = InvoiceRemoteDataSource(erpService) - invoiceRepository = InvoiceRepository( + invoiceRepository = DefaultInvoiceRepository( invoiceRemoteDataSource, invoiceLocalDataSource, coroutineRule.dispatchers diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoicesTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoicesTest.kt new file mode 100644 index 00000000..f48eee73 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/usecase/DeleteAllLocalInvoicesTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class DeleteAllLocalInvoicesTest { + private val repository = mockk() + private val dispatcher = StandardTestDispatcher() + private lateinit var useCase: DeleteAllLocalInvoices + + @Before + fun setup() { + coEvery { repository.deleteLocalInvoice(any()) } returns Unit + useCase = DeleteAllLocalInvoices( + invoiceRepository = repository, + dispatcher = dispatcher + ) + } + + @Test + fun testDeleteInvoice() { + runTest { + useCase.invoke(listOf("123", "234")) + coVerify(exactly = 1) { + repository.deleteLocalInvoice("123") + } + coVerify(exactly = 2) { + repository.deleteLocalInvoice("234") + } + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/MedicationPlanMocks.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/MedicationPlanMocks.kt new file mode 100644 index 00000000..38e47918 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/MedicationPlanMocks.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan + +import de.gematik.ti.erp.app.medicationplan.model.MedicationDosage +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotification +import de.gematik.ti.erp.app.medicationplan.model.MedicationNotificationMessage +import de.gematik.ti.erp.app.medicationplan.model.MedicationSchedule +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.utils.toLocalDate +import io.mockk.mockk +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalTime +import kotlin.time.Duration.Companion.days + +val scannedTaskAmount = Ratio(Quantity("1", ""), Quantity("1", "")) +val syncedTaskAmount = Ratio(Quantity("1", ""), Quantity("1", "")) + +val MEDICATION_SCHEDULE = MedicationSchedule( + start = Instant.parse("2024-01-01T08:00:00Z").toLocalDate(), + end = Instant.parse("2024-01-01T20:00:00Z").toLocalDate(), + isActive = true, + message = MedicationNotificationMessage("title", "body"), + taskId = "taskId", + profileId = "profileId", + amount = scannedTaskAmount, + notifications = listOf( + MedicationNotification( + time = LocalTime(8, 0), + dosage = MedicationDosage("tablet", "1"), + id = "1234" + ) + ) +) + +val profile1 = ProfilesData.Profile( + id = "PROFILE_ID1", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = null, + name = "Erna Mustermann", + insurantName = "Erna Mustermann", + insuranceIdentifier = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + lastAuthenticated = mockk(), + lastTaskSynced = mockk(), + active = true, + singleSignOnTokenScope = null +) + +val profile2 = ProfilesData.Profile( + id = "PROFILE_ID2", + color = ProfilesData.ProfileColorNames.PINK, + avatar = ProfilesData.Avatar.Baby, + image = null, + name = "Erna P", + insurantName = "Erna P", + insuranceIdentifier = "AOK", + insuranceType = ProfilesData.InsuranceType.GKV, + isConsentDrawerShown = true, + lastAuthenticated = mockk(), + lastTaskSynced = mockk(), + active = true, + singleSignOnTokenScope = null +) + +val medicationSchedule1 = MedicationSchedule( + start = Instant.parse("2024-01-01T08:00:00Z").toLocalDate(), + end = Instant.parse("2024-01-01T20:00:00Z").toLocalDate(), + isActive = true, + message = MedicationNotificationMessage("title", "body"), + taskId = "taskId", + profileId = "PROFILE_ID1", + amount = scannedTaskAmount, + notifications = listOf( + MedicationNotification(id = "1", time = LocalTime(8, 0), dosage = MedicationDosage("Dosis", "1")), + MedicationNotification(id = "2", time = LocalTime(12, 0), dosage = MedicationDosage("Dosis", "1")) + ) +) + +val medicationSchedule2 = MedicationSchedule( + start = Instant.parse("2024-01-01T08:00:00Z").toLocalDate(), + end = Instant.parse("2024-01-01T20:00:00Z").toLocalDate(), + isActive = true, + message = MedicationNotificationMessage("title", "body"), + taskId = "taskId", + profileId = "PROFILE_ID2", + amount = syncedTaskAmount, + notifications = listOf( + MedicationNotification(id = "3", time = LocalTime(10, 5), dosage = MedicationDosage("Dosis", "1")), + MedicationNotification(id = "4", time = LocalTime(18, 0), dosage = MedicationDosage("Dosis", "1")) + ) +) + +private val MEDICATION = SyncedTaskData.Medication( + category = SyncedTaskData.MedicationCategory.entries[0], + vaccine = true, + text = "Medication", + form = "AEO", + lotNumber = "123456", + expirationDate = FhirTemporal.Instant(Clock.System.now().plus(30.days)), + identifier = SyncedTaskData.Identifier("1234567890"), + ingredientMedications = emptyList(), + ingredients = emptyList(), + normSizeCode = "KA", + amount = Ratio( + numerator = Quantity( + value = "1", + unit = "oz" + ), + denominator = null + ), + manufacturingInstructions = null, + packaging = null +) + +internal var MEDICATION_REQUEST = MedicationRequest( + medication = MEDICATION, + dateOfAccident = null, + location = "Location", + emergencyFee = true, + dosageInstruction = "Dosage", + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = "Note", + substitutionAllowed = true +) + +internal val PRACTITIONER = Practitioner( + name = "Dr. Max Mustermann", + qualification = "Arzt", + practitionerIdentifier = "1234567890" +) + +internal val INSURANCE_INFO = SyncedTaskData.InsuranceInformation( + name = "AOK", + status = "status", + coverageType = SyncedTaskData.CoverageType.GKV +) + +internal val ADDRESS = SyncedTaskData.Address( + line1 = "Hauptstraße 1", + line2 = "12345 Musterstadt", + postalCode = "12345", + city = "Musterstadt" +) + +internal val PATIENT = Patient( + name = "Erna Mustermann", + address = ADDRESS, + birthdate = null, + insuranceIdentifier = "AOK" +) + +internal val ORGANIZATION = Organization( + name = "Praxis Dr. Mustermann", + address = ADDRESS, + uniqueIdentifier = "1234567890", + phone = "0123456789", + mail = "mustermann@praxis.de" +) + +val scannedTask = ScannedTaskData.ScannedTask( + profileId = "PROFILE_ID", + taskId = "active-scanned-task-id-1", + index = 0, + name = "Scanned Task", + accessCode = "1234", + scannedOn = Instant.parse("2024-01-01T08:00:00Z"), + redeemedOn = null, + communications = emptyList() +) + +val syncedTask = SyncedTaskData.SyncedTask( + profileId = "PROFILE_ID", + taskId = "active-synced-task-id-1", + accessCode = "1234", + lastModified = Instant.parse("2024-01-01T08:00:00Z"), + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = INSURANCE_INFO, + expiresOn = Instant.parse("2024-01-01T08:00:00Z"), + acceptUntil = Instant.parse("2024-01-01T08:00:00Z"), + authoredOn = Instant.parse("2024-01-01T08:00:00Z"), + status = SyncedTaskData.TaskStatus.Ready, // to be active prescription + isIncomplete = false, + pvsIdentifier = "pvsIdentifier", + failureToReport = "failureToReport", + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = emptyList(), + lastMedicationDispense = null, + communications = emptyList() +) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapperTest.kt new file mode 100644 index 00000000..3804cbab --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/model/MedicationPlanMapperTest.kt @@ -0,0 +1,656 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.model + +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationDosageEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationNotificationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.medicationplan.MedicationScheduleEntityV1 +import de.gematik.ti.erp.app.medicationplan.MEDICATION_REQUEST +import de.gematik.ti.erp.app.medicationplan.MEDICATION_SCHEDULE +import de.gematik.ti.erp.app.medicationplan.scannedTask +import de.gematik.ti.erp.app.medicationplan.syncedTask +import de.gematik.ti.erp.app.prescription.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.model.Quantity +import de.gematik.ti.erp.app.prescription.model.Ratio +import de.gematik.ti.erp.app.utils.toLocalDate +import io.realm.kotlin.ext.toRealmList +import junit.framework.TestCase.assertNotNull +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.days + +class MedicationPlanMapperTest { + + @Test + fun `test null dosage instruction`() { + val result = parseInstruction(null) + assertEquals(MedicationPlanDosageInstruction.Empty, result) + } + + @Test + fun `test empty dosage instruction`() { + val result = parseInstruction("") + assertEquals(MedicationPlanDosageInstruction.Empty, result) + } + + @Test + fun `test empty dosage instruction an space`() { + val result = parseInstruction(" ") + assertEquals(MedicationPlanDosageInstruction.Empty, result) + } + + @Test + fun `test free text dosage instruction`() { + val result = parseInstruction("Take one pill in the morning") + assertEquals( + MedicationPlanDosageInstruction.FreeText("Take one pill in the morning"), + result + ) + } + + @Test + fun `test once in the morning 3 parts`() { + val result = parseInstruction("1-0-0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) + ), + result + ) + } + + @Test + fun `test 1 in the morning, 1 in the evening 3 parts`() { + val result = parseInstruction("1-0-1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + result + ) + } + + @Test + fun `test 1 in the morning, 1 2 on noon, 1 in the evening 3 parts`() { + val result = parseInstruction("1 - 2- 3") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1 - 2- 3", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.NOON to "2", + MedicationPlanDosageInstruction.DayTime.EVENING to "3" + ) + ), + result + ) + } + + @Test + fun `test 3 float & fraction types`() { + val result = parseInstruction("1.5-1,5-1½") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1.5-1,5-1½", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1.5", + MedicationPlanDosageInstruction.DayTime.NOON to "1,5", + MedicationPlanDosageInstruction.DayTime.EVENING to "1½" + ) + ), + result + ) + } + + @Test + fun `test fraction types with spaces`() { + val result = parseInstruction("½ - 2 ½ - 3 ½") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "½ - 2 ½ - 3 ½", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "½", + MedicationPlanDosageInstruction.DayTime.NOON to "2 ½", + MedicationPlanDosageInstruction.DayTime.EVENING to "3 ½" + ) + ), + result + ) + } + + @Test + fun `test 3 parts with empty middle part`() { + val result = parseInstruction("1--1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1--1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + result + ) + } + + @Test + fun `test 3 parts with empty middle part and 0 on first`() { + val result = parseInstruction("0--1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "0--1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + result + ) + } + + @Test + fun `test 3 parts with 1 on middle part`() { + val result = parseInstruction("-1-") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "-1-", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.NOON to "1" + ) + ), + result + ) + } + + @Test + fun `test 3 parts with angle brackets`() { + val result = parseInstruction("<<1-0-0>>") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "<<1-0-0>>", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) + ), + result + ) + } + + @Test + fun `test 3 parts with angle brackets and spaces`() { + val result = parseInstruction(" >> << 1-0-0 <<> > ") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = " >> << 1-0-0 <<> > ", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 1 Morning`() { + val result = parseInstruction("1-0-0-0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-0-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 1 Morning, 1 Evening`() { + val result = parseInstruction("1-0-1-0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 1 Noon, 1 Night`() { + val result = parseInstruction("0-1-0-1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "0-1-0-1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.NOON to "1", + MedicationPlanDosageInstruction.DayTime.NIGHT to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts with one float and spaces`() { + val result = parseInstruction("1 - 2- 3- 4.0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1 - 2- 3- 4.0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.NOON to "2", + MedicationPlanDosageInstruction.DayTime.EVENING to "3", + MedicationPlanDosageInstruction.DayTime.NIGHT to "4.0" + ) + ), + result + ) + } + + @Test + fun `test 4 parts with float, spaces and fraction types`() { + val result = parseInstruction("1.5 - 1,5 - 2 ½ - ½") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1.5 - 1,5 - 2 ½ - ½", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1.5", + MedicationPlanDosageInstruction.DayTime.NOON to "1,5", + MedicationPlanDosageInstruction.DayTime.EVENING to "2 ½", + MedicationPlanDosageInstruction.DayTime.NIGHT to "½" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 1 morning 1 night `() { + val result = parseInstruction("1---1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1---1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.NIGHT to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 1 morning 1 night 1 empty `() { + val result = parseInstruction("1--0-1") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1--0-1", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.NIGHT to "1" + ) + ), + result + ) + } + + @Test + fun `test 4 parts 2 noon `() { + val result = parseInstruction("-2--0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "-2--0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.NOON to "2" + ) + ), + result + ) + } + + @Test + fun `test dj with angle brackets`() { + val result = parseInstruction("< << DJ > >>") + assertEquals( + MedicationPlanDosageInstruction.External, + result + ) + } + + @Test + fun `test with text`() { + val result = parseInstruction("1-0-x-0") + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-x-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1" + ) + ), + result + ) + } + + @Test + fun `test with all 0 should be empty`() { + val result = parseInstruction("0-0-0") + assertEquals( + MedicationPlanDosageInstruction.Empty, + result + ) + } + + @Test + fun `test with words`() { + val result = parseInstruction("2 mal Mörgens") + assertEquals( + MedicationPlanDosageInstruction.FreeText( + text = "2 mal Mörgens" + ), + result + ) + } + + @Test + fun `test dosage instruction with more than 4 parts should return Freetext`() { + val result = parseInstruction("1-2-1-1-1-1-2-0") + assertEquals( + MedicationPlanDosageInstruction.FreeText( + text = "1-2-1-1-1-1-2-0" + ), + result + ) + } + + @Test + fun `MedicationDosageEntityV1 to MedicationDosage`() { + val dosageEntity = MedicationDosageEntityV1().apply { + form = "tablet" + ratio = "1" + } + val dosage = dosageEntity.toMedicationDosage() + assertEquals("tablet", dosage.form) + assertEquals("1", dosage.ratio) + } + + @Test + fun `RealmList of notificationEntities to list of Notifications`() { + val now = Instant.parse("2024-01-01T08:00:00Z") + val notificationEntity = MedicationNotificationEntityV1().apply { + time = now.toLocalDateTime(TimeZone.of("Europe/Berlin")).toString() + dosage = MedicationDosageEntityV1().apply { + form = "tablet" + ratio = "1" + } + id = "1234" + } + val realmList = listOf(notificationEntity).toRealmList() + val notifications = realmList.toNotifications() + assertEquals("09:00", notifications[0].time.toString()) + assertEquals(1, notifications.size) + assertEquals("1234", notifications[0].id) + assertEquals("tablet", notifications[0].dosage.form) + assertEquals("1", notifications[0].dosage.ratio) + } + + @Test + fun `update MedicationScheduleEntityV1`() { + val scheduleEntity = MedicationScheduleEntityV1() + val newSchedule = MEDICATION_SCHEDULE + scheduleEntity.update(newSchedule) + assertEquals("title", scheduleEntity.title) + assertEquals("body", scheduleEntity.body) + assertEquals("taskId", scheduleEntity.taskId) + assertEquals(1, scheduleEntity.notifications.size) + assertEquals("tablet", scheduleEntity.notifications[0].dosage?.form) + assertEquals("1", scheduleEntity.notifications[0].dosage?.ratio) + } + + @Test + fun `MedicationScheduleEntityV1 to MedicationSchedule`() { + val scheduleEntity = MedicationScheduleEntityV1().apply { + val now = Instant.parse("2024-01-01T08:00:00Z") + + start = now.toLocalDate().toString() + end = now.toLocalDate().toString() + isActive = true + title = "title" + body = "body" + taskId = "taskId" + profileId = "profileId" + notifications = listOf( + MedicationNotificationEntityV1().apply { + time = now.toLocalDateTime(TimeZone.of("Europe/Berlin")).toString() + dosage = MedicationDosageEntityV1().apply { + form = "tablet" + ratio = "1" + } + id = "1234" + } + ).toRealmList() + } + val schedule = scheduleEntity.toMedicationSchedule() + assertEquals("title", schedule.message.title) + assertEquals("body", schedule.message.body) + assertEquals("taskId", schedule.taskId) + assertEquals(1, schedule.notifications.size) + assertEquals("tablet", schedule.notifications[0].dosage.form) + assertEquals("1", schedule.notifications[0].dosage.ratio) + } + + @Test + fun `ScannedPrescription to MedicationSchedule`() { + val scannedPrescription = PrescriptionData.Scanned(scannedTask) + val schedule = scannedPrescription.toMedicationSchedule() + assertEquals("Scanned Task", schedule.message.title) + assertEquals("", schedule.message.body) + assertEquals("active-scanned-task-id-1", schedule.taskId) + assertEquals(0, schedule.notifications.size) + } + + @Test + fun `SyncedPrescription with freetext dosage instruction to MedicationSchedule`() { + val syncedPrescription = PrescriptionData.Synced(syncedTask) + val schedule = syncedPrescription.toMedicationSchedule() + assertEquals("Medication", schedule.message.title) + assertEquals("", schedule.message.body) + assertEquals("active-synced-task-id-1", schedule.taskId) + assertEquals(0, schedule.notifications.size) + } + + @Test + fun `SyncedPrescription with structured dosage instruction to MedicationSchedule`() { + val syncedPrescription = PrescriptionData.Synced( + syncedTask.copy( + medicationRequest = MEDICATION_REQUEST.copy( + dosageInstruction = "1-0-1" + ) + ) + ) + val schedule = syncedPrescription.toMedicationSchedule() + assertEquals("Medication", schedule.message.title) + assertEquals("", schedule.message.body) + assertEquals("active-synced-task-id-1", schedule.taskId) + assertEquals(2, schedule.notifications.size) + } + + @Test + fun `multiply medicationAmount with float string`() { + val ratio = Ratio( + numerator = Quantity(value = "0.5", unit = "mg"), + denominator = Quantity(value = "1", unit = "") + ) + val result = multiplyMedicationAmount(ratio, 2) + assertNotNull(result) + assertEquals("1", result?.numerator?.value) + } + + @Test + fun `multiply medicationAmount with int string`() { + val ratio = Ratio( + numerator = Quantity(value = "10", unit = "mg"), + denominator = Quantity(value = "1", unit = "") + ) + val result = multiplyMedicationAmount(ratio, 2) + assertNotNull(result) + assertEquals("20", result?.numerator?.value) + } + + @Test + fun `multiply medicationAmount with comma string`() { + val ratio = Ratio( + numerator = Quantity(value = "0,7", unit = "mg"), + denominator = Quantity(value = "1", unit = "") + ) + val result = multiplyMedicationAmount(ratio, 2) + assertEquals("1.4", result?.numerator?.value) + } + + @Test + fun `multiply medicationAmount with null value`() { + val result = multiplyMedicationAmount(null, 2) + assert(result == null) + } + + @Test + fun `multiply medicationAmount with int Result`() { + val ratio = Ratio( + numerator = Quantity(value = "1.0", unit = "mg"), + denominator = Quantity(value = "1", unit = "") + ) + val result = multiplyMedicationAmount(ratio, 2) + assertEquals("2", result?.numerator?.value) + } + + @Test + fun `get calculated end date with empty dosage instructions`() { + val start = Instant.parse("2024-01-01T08:00:00Z") + val endDate = getCalculatedEndDate( + start = start, + amount = Ratio( + numerator = Quantity(value = "30", unit = ""), + denominator = Quantity(value = "1", unit = "") + ), + dosageInstruction = MedicationPlanDosageInstruction.Empty, + form = "TAB" + ) + assertEquals(start.toLocalDate(), endDate) + } + + @Test + fun `get calculated end date with structured dosage instructions 1-0-1-0 and not pieceable Form form`() { + val start = Instant.parse("2024-01-01T00:00:00Z") + val endDate = getCalculatedEndDate( + start = start, + amount = Ratio( + numerator = Quantity(value = "30", unit = ""), + denominator = Quantity(value = "1", unit = "") + ), + dosageInstruction = MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + form = "NOT_PIECEABLE" + ) + assertEquals(start.toLocalDate(), endDate) + } + + @Test + fun `get calculated end date with structured dosage instructions 1-0-1-0 start in the morning`() { + val start = Instant.parse("2024-01-01T00:00:00Z") + val expectedEndDate = start.plus(14.days) + val endDate = getCalculatedEndDate( + start = start, + amount = Ratio( + numerator = Quantity(value = "30", unit = ""), + denominator = Quantity(value = "1", unit = "") + ), + dosageInstruction = MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + form = "TAB" + ) + assertEquals(expectedEndDate.toLocalDate(), endDate) + } + + @Test + fun `get calculated end date with structured dosage instructions ½-0-½-0 start in the morning`() { + val start = Instant.parse("2024-01-01T00:00:00Z") + val expectedEndDate = start.plus(29.days) + val endDate = getCalculatedEndDate( + start = start, + amount = Ratio( + numerator = Quantity(value = "30", unit = ""), + denominator = Quantity(value = "1", unit = "") + ), + dosageInstruction = MedicationPlanDosageInstruction.Structured( + text = "½-0-½-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "0.5", + MedicationPlanDosageInstruction.DayTime.EVENING to "0.5" + ) + ), + form = "TAB" + ) + println(endDate) + assertEquals(expectedEndDate.toLocalDate(), endDate) + } + + @Test + fun `get calculated end date with structured dosage instructions 1-0-1-0 start in the evening`() { + val start = Instant.parse("2024-01-01T17:00:00Z") + val expectedEndDate = start.plus(15.days) + val endDate = getCalculatedEndDate( + start = start, + amount = Ratio( + numerator = Quantity(value = "30", unit = ""), + denominator = Quantity(value = "1", unit = "") + ), + dosageInstruction = MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + form = "TAB" + ) + assertEquals(expectedEndDate.toLocalDate(), endDate) + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepositoryTest.kt new file mode 100644 index 00000000..4d17dc4d --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/repository/MedicationPlanRepositoryTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.repository + +import de.gematik.ti.erp.app.medicationplan.MEDICATION_SCHEDULE +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class MedicationPlanRepositoryTest { + + private val localDataSource: MedicationPlanLocalDataSource = mockk() + lateinit var repository: MedicationPlanRepository + + @Before + fun setup() { + repository = MedicationPlanRepository(localDataSource) + } + + @Test + fun `load medicationSchedule`() { + val medicationSchedule = MEDICATION_SCHEDULE + coEvery { localDataSource.loadMedicationSchedule(any()) } returns flowOf(medicationSchedule) + runTest { + val result = repository.loadMedicationSchedule("taskId").first() + assertEquals(medicationSchedule, result) + } + } + + @Test + fun `update medicationSchedule`() = runTest { + val newMedicationSchedule = MEDICATION_SCHEDULE + coEvery { localDataSource.updateMedicationSchedule(newMedicationSchedule) } returns Unit + + repository.updateMedicationSchedule(newMedicationSchedule) + coVerify(exactly = 1) { localDataSource.updateMedicationSchedule(newMedicationSchedule) } + } + + @Test + fun `load all medicationSchedules`() = runTest { + val newMedicationSchedule = MEDICATION_SCHEDULE + coEvery { localDataSource.loadAllMedicationSchedules() } returns flowOf(listOf(MEDICATION_SCHEDULE)) + + repository.loadAllMedicationSchedules() + coVerify(exactly = 2) { localDataSource.loadAllMedicationSchedules() } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCaseTest.kt new file mode 100644 index 00000000..20a5cf88 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/GetDosageInstructionByTaskIdUseCaseTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.MEDICATION_REQUEST +import de.gematik.ti.erp.app.medicationplan.model.MedicationPlanDosageInstruction +import de.gematik.ti.erp.app.medicationplan.scannedTask +import de.gematik.ti.erp.app.medicationplan.syncedTask +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetDosageInstructionByTaskIdUseCaseTest { + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private val prescriptionRepository: PrescriptionRepository = mockk() + private lateinit var useCase: GetDosageInstructionByTaskIdUseCase + + @BeforeTest + fun setup() { + useCase = GetDosageInstructionByTaskIdUseCase( + repository = prescriptionRepository + ) + } + + @Test + fun `scanned prescription should return empty dosage instruction`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns flowOf(scannedTask) + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns emptyFlow() + + testScope.runTest { + val dosageInstruction = useCase.invoke("taskId").first() + assertEquals(MedicationPlanDosageInstruction.Empty, dosageInstruction) + } + } + + @Test + fun `synced prescription should return empty dosage instruction`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf( + syncedTask.copy( + medicationRequest = MEDICATION_REQUEST.copy(dosageInstruction = null) + ) + ) + + testScope.runTest { + val dosageInstruction = useCase.invoke("taskId").first() + assertEquals(MedicationPlanDosageInstruction.Empty, dosageInstruction) + } + } + + @Test + fun `synced prescription should return freetext dosage instruction`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf( + syncedTask.copy( + medicationRequest = MEDICATION_REQUEST.copy(dosageInstruction = "freetext") + ) + ) + + testScope.runTest { + val dosageInstruction = useCase.invoke("taskId").first() + assertEquals(MedicationPlanDosageInstruction.FreeText("freetext"), dosageInstruction) + } + } + + @Test + fun `synced prescription should return structured dosage instruction`() { + coEvery { prescriptionRepository.loadScannedTaskByTaskId(any()) } returns emptyFlow() + coEvery { prescriptionRepository.loadSyncedTaskByTaskId(any()) } returns flowOf( + syncedTask.copy( + medicationRequest = MEDICATION_REQUEST.copy(dosageInstruction = "1-0-1-0") + ) + ) + + testScope.runTest { + val dosageInstruction = useCase.invoke("taskId").first() + assertEquals( + MedicationPlanDosageInstruction.Structured( + text = "1-0-1-0", + interpretation = mapOf( + MedicationPlanDosageInstruction.DayTime.MORNING to "1", + MedicationPlanDosageInstruction.DayTime.EVENING to "1" + ) + ), + dosageInstruction + ) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCaseTest.kt new file mode 100644 index 00000000..e86d27dc --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadAllMedicationSchedulesUseCaseTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.MEDICATION_SCHEDULE +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanLocalDataSource +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LoadAllMedicationSchedulesUseCaseTest { + private val dispatcher = StandardTestDispatcher() + private val medicationPlanLocalDataSource: MedicationPlanLocalDataSource = mockk() + private lateinit var medicationPlanRepository: MedicationPlanRepository + private lateinit var useCase: LoadAllMedicationSchedulesUseCase + + @BeforeTest + fun setup() { + medicationPlanRepository = MedicationPlanRepository( + localDataSource = medicationPlanLocalDataSource + ) + useCase = LoadAllMedicationSchedulesUseCase( + medicationPlanRepository = medicationPlanRepository + ) + coEvery { medicationPlanRepository.loadAllMedicationSchedules() } returns flowOf(listOf(MEDICATION_SCHEDULE)) + } + + @Test + fun `get medication plans for profile`() { + runTest(dispatcher) { + val medicationSchedules = useCase.invoke().first() + assertEquals(listOf(MEDICATION_SCHEDULE), medicationSchedules) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCaseTest.kt new file mode 100644 index 00000000..32916bc3 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadMedicationScheduleByTaskIdUseCaseTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.MEDICATION_SCHEDULE +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanLocalDataSource +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlin.test.assertEquals +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LoadMedicationScheduleByTaskIdUseCaseTest { + private val dispatcher = StandardTestDispatcher() + private val medicationPlanLocalDataSource: MedicationPlanLocalDataSource = mockk() + private lateinit var medicationPlanRepository: MedicationPlanRepository + private lateinit var useCase: LoadMedicationScheduleByTaskIdUseCase + + @BeforeTest + fun setup() { + medicationPlanRepository = MedicationPlanRepository( + localDataSource = medicationPlanLocalDataSource + ) + useCase = LoadMedicationScheduleByTaskIdUseCase( + medicationPlanRepository = medicationPlanRepository + ) + coEvery { medicationPlanRepository.loadMedicationSchedule(any()) } returns flowOf(MEDICATION_SCHEDULE) + } + + @Test + fun `get medication plans for profile`() { + runTest(dispatcher) { + val medicationScheduleByTaskId = useCase.invoke("TaskId").first() + assertEquals(MEDICATION_SCHEDULE, medicationScheduleByTaskId) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCaseTest.kt new file mode 100644 index 00000000..1c487cf6 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/medicationplan/usecase/LoadProfilesWithSchedulesUseCaseTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.medicationplan.usecase + +import de.gematik.ti.erp.app.medicationplan.medicationSchedule1 +import de.gematik.ti.erp.app.medicationplan.medicationSchedule2 +import de.gematik.ti.erp.app.medicationplan.profile1 +import de.gematik.ti.erp.app.medicationplan.profile2 + +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanLocalDataSource +import de.gematik.ti.erp.app.medicationplan.repository.MedicationPlanRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import io.mockk.MockKAnnotations +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDateTime +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LoadProfilesWithSchedulesUseCaseTest { + private val dispatcher = StandardTestDispatcher() + private val medicationPlanLocalDataSource: MedicationPlanLocalDataSource = mockk() + private val profileRepository: ProfileRepository = mockk() + private lateinit var medicationPlanRepository: MedicationPlanRepository + private lateinit var useCase: LoadProfilesWithSchedulesUseCase + + @BeforeTest + fun setup() { + MockKAnnotations.init(this) + medicationPlanRepository = MedicationPlanRepository( + localDataSource = medicationPlanLocalDataSource + ) + useCase = LoadProfilesWithSchedulesUseCase( + profileRepository = profileRepository, + medicationPlanRepository = medicationPlanRepository + ) + } + + @Test + fun `invoke with current date time - sorts ProfileWithSchedules correctly`() { + val now = LocalDateTime.parse("2024-01-01T12:00:00") + coEvery { profileRepository.profiles() } returns flowOf(listOf(profile1, profile2)) + coEvery { medicationPlanLocalDataSource.loadAllMedicationSchedules() } returns + flowOf(listOf(medicationSchedule1, medicationSchedule2)) + + runTest(dispatcher) { + val result = useCase.invoke(now).first() + assertEquals(2, result.size) + val firstProfileWithSchedules = result[0] + assertEquals("PROFILE_ID2", firstProfileWithSchedules.profile.id) // Profile 2 should be first + val firstNotifications = firstProfileWithSchedules.medicationSchedules[0].notifications + assertEquals("3", firstNotifications[0].id) + assertEquals("4", firstNotifications[1].id) + println("firstNotifications: $firstNotifications") + + val secondProfileWithSchedules = result[1] + assertEquals("PROFILE_ID1", secondProfileWithSchedules.profile.id) + val secondNotifications = secondProfileWithSchedules.medicationSchedules[0].notifications + assertEquals("2", secondNotifications[0].id) + assertEquals("1", secondNotifications[1].id) // overflow + } + } + + @Test + fun `invoke without date time - sorts ProfileWithSchedules correctly`() { + coEvery { profileRepository.profiles() } returns flowOf(listOf(profile1, profile2)) + coEvery { medicationPlanLocalDataSource.loadAllMedicationSchedules() } returns + flowOf(listOf(medicationSchedule1, medicationSchedule2)) + + runTest(dispatcher) { + val result = useCase.invoke().first() + assertEquals(2, result.size) + val firstProfileWithSchedules = result[0] + assertEquals("PROFILE_ID1", firstProfileWithSchedules.profile.id) // Profile 2 should be first + val firstNotifications = firstProfileWithSchedules.medicationSchedules[0].notifications + assertEquals("1", firstNotifications[0].id) + assertEquals("2", firstNotifications[1].id) + println("firstNotifications: $firstNotifications") + + val secondProfileWithSchedules = result[1] + assertEquals("PROFILE_ID2", secondProfileWithSchedules.profile.id) + val secondNotifications = secondProfileWithSchedules.medicationSchedules[0].notifications + assertEquals("3", secondNotifications[0].id) + assertEquals("4", secondNotifications[1].id) // overflow + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/CardUtilitiesTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/CardUtilitiesTest.kt index b17d0a5b..00b4b654 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/CardUtilitiesTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/CardUtilitiesTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/KeyDerivationFunctionTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/KeyDerivationFunctionTest.kt index abb5e5fb..704719dc 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/KeyDerivationFunctionTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/KeyDerivationFunctionTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/PaceInfoTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/PaceInfoTest.kt index 277c2b06..1b50a39c 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/PaceInfoTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/PaceInfoTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/SecureMessagingTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/SecureMessagingTest.kt index a2f5bd5e..dfe95c41 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/SecureMessagingTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/SecureMessagingTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("ktlint:max-line-length") diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/card/Version2Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/card/Version2Test.kt index 1cc40c26..8360fd2d 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/card/Version2Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/card/Version2Test.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.card diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/CommandApduTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/CommandApduTest.kt index 34227a57..c2821d93 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/CommandApduTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/CommandApduTest.kt @@ -1,28 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ +@file:Suppress("VariableNaming") + package de.gematik.ti.erp.app.nfc.command import de.gematik.ti.erp.app.card.model.command.CommandApdu import de.gematik.ti.erp.app.card.model.command.EXPECTED_LENGTH_WILDCARD_EXTENDED import de.gematik.ti.erp.app.card.model.command.EXPECTED_LENGTH_WILDCARD_SHORT import org.junit.Assert -import kotlin.test.Test +import org.junit.Test import java.util.Arrays import java.util.Random diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/GeneralAuthenticateCommandTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/GeneralAuthenticateCommandTest.kt index 50510145..134a9276 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/GeneralAuthenticateCommandTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/GeneralAuthenticateCommandTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ManageSecurityEnvironmentCommandTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ManageSecurityEnvironmentCommandTest.kt index a2894287..84f93046 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ManageSecurityEnvironmentCommandTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ManageSecurityEnvironmentCommandTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ReadCommandTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ReadCommandTest.kt index b7beed34..d5a0e57d 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ReadCommandTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ReadCommandTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ResponseApduTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ResponseApduTest.kt index 7d88f207..e5828fdc 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ResponseApduTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/ResponseApduTest.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command import de.gematik.ti.erp.app.card.model.command.ResponseApdu import org.junit.Assert -import kotlin.test.Test +import org.junit.Test import java.util.Arrays import java.util.Random diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/SelectCommandTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/SelectCommandTest.kt index 20c9af78..05349ea6 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/SelectCommandTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/SelectCommandTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestChannel.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestChannel.kt index 0ddf53bb..ddcecb11 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestChannel.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestChannel.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestResource.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestResource.kt index 83ca3c18..3d204ca4 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestResource.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/command/TestResource.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") +@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping", "UnusedPrivateMember") package de.gematik.ti.erp.app.nfc.command diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/FileIdentifierTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/FileIdentifierTest.kt index 292ee9c8..d5f8e96d 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/FileIdentifierTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/FileIdentifierTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.identifier diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/ShortFileIdentifierTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/ShortFileIdentifierTest.kt index f72d4008..64e1dd68 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/ShortFileIdentifierTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/nfc/identifier/ShortFileIdentifierTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.nfc.identifier diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCaseTest.kt index c1cb30a5..7cd9f8c3 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetShippingContactValidationUseCaseTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.pharmacy.usecase diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt index f1914edf..763129d5 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt @@ -1,26 +1,26 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.profiles.repository import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.db.TestDB import de.gematik.ti.erp.app.db.ACTUAL_SCHEMA_VERSION +import de.gematik.ti.erp.app.db.TestDB import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 import de.gematik.ti.erp.app.db.entities.v1.IdpAuthenticationDataEntityV1 import de.gematik.ti.erp.app.db.entities.v1.PasswordEntityV1 @@ -49,13 +49,16 @@ import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 import de.gematik.ti.erp.app.profiles.model.ProfilesData import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock +import org.junit.After +import org.junit.Before import org.junit.Rule -import kotlin.test.Test -import kotlin.test.BeforeTest +import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -76,7 +79,7 @@ class ProfilesRepositoryTest : TestDB() { lateinit var repo: DefaultProfilesRepository - @BeforeTest + @Before fun setUp() { realm = Realm.open( RealmConfiguration.Builder( @@ -119,6 +122,11 @@ class ProfilesRepositoryTest : TestDB() { ) } + @After + fun tearDown() { + realm.close() + } + @Test fun `profiles should return empty list `() = runTest { repo.profiles().first().also { @@ -174,7 +182,7 @@ class ProfilesRepositoryTest : TestDB() { profile.name == defaultProfileName }.apply { this?.let { - repo.removeProfile(it.id) + repo.removeProfile(it.id, defaultProfileName) } } } @@ -186,7 +194,7 @@ class ProfilesRepositoryTest : TestDB() { } @Test - fun `remove last profile - should fail`() = runTest { + fun `remove last profile - should create a default profile`() = runTest { repo.saveProfile(defaultProfileName, true) repo.profiles().first().also { profileList -> assertEquals(1, profileList.size) @@ -194,12 +202,15 @@ class ProfilesRepositoryTest : TestDB() { profile.name == defaultProfileName }.apply { this?.let { - assertFails { - repo.removeProfile(it.id) - } + repo.removeProfile(it.id, defaultProfileName1) } } } + repo.profiles().first().also { profileList -> + assertEquals(1, profileList.size) + assertEquals(actual = profileList.first().name, expected = defaultProfileName1) + assertEquals(actual = profileList.first().active, expected = true) + } } @Test @@ -274,6 +285,58 @@ class ProfilesRepositoryTest : TestDB() { } } + @Test + fun `saveInsuranceInformation when isNewlyCreated is true should set insurantName as profile name`() = runTest { + repo.saveProfile(defaultProfileName, true) + + realm.write { + val profileEntity = realm.query(ProfileEntityV1::class, "name == $0", defaultProfileName).first().find() + profileEntity?.isNewlyCreated = true + } + + val profileId = repo.profiles().first().find { it.name == defaultProfileName }?.id + if (profileId != null) { + repo.saveInsuranceInformation( + profileId = profileId, + insurantName = defaultInsurantName, + insuranceIdentifier = defaultInsuranceIdentifier, + insuranceName = defaultInsuranceName + ) + } + + realm.query(ProfileEntityV1::class, "insuranceIdentifier == $0", defaultInsuranceIdentifier).first().find() + ?.let { updatedProfile -> + assertEquals(defaultInsurantName, updatedProfile.name) + assertTrue(updatedProfile.isNewlyCreated) + } + } + + @Test + fun `saveInsuranceInformation when isNewlyCreated is false should retain existing profile name`() = runTest { + repo.saveProfile(defaultProfileName, true) + + realm.write { + val profileEntity = realm.query(ProfileEntityV1::class, "name == $0", defaultProfileName).first().find() + profileEntity?.isNewlyCreated = false + } + + val profileId = repo.profiles().first().find { it.name == defaultProfileName }?.id + if (profileId != null) { + repo.saveInsuranceInformation( + profileId = profileId, + insurantName = defaultInsurantName, + insuranceIdentifier = defaultInsuranceIdentifier, + insuranceName = defaultInsuranceName + ) + } + + realm.query(ProfileEntityV1::class, "insuranceIdentifier == $0", defaultInsuranceIdentifier).first().find() + ?.let { updatedProfile -> + assertEquals(defaultProfileName, updatedProfile.name) + assertFalse(updatedProfile.isNewlyCreated) + } + } + @Test fun `update profile name with id`() = runTest { repo.saveProfile(defaultProfileName, true) @@ -329,11 +392,11 @@ class ProfilesRepositoryTest : TestDB() { val profileImage = byteArrayOf(0x01.toByte(), 0x02.toByte()) repo.saveProfile(defaultProfileName, true) repo.profiles().first().also { - assertEquals(null, it[0].personalizedImage) + assertEquals(null, it[0].image) repo.savePersonalizedProfileImage(it[0].id, profileImage) } repo.profiles().first().also { - it[0].personalizedImage?.let { bytes -> + it[0].image?.let { bytes -> assertEquals(0x01.toByte(), bytes[0]) assertEquals(0x02.toByte(), bytes[1]) } @@ -351,7 +414,7 @@ class ProfilesRepositoryTest : TestDB() { repo.clearPersonalizedProfileImage(it[0].id) } repo.profiles().first().also { - assertEquals(null, it[0].personalizedImage) + assertEquals(null, it[0].image) } } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt index b9021e70..a2841700 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.protocol.repository diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepositoryTest.kt new file mode 100644 index 00000000..448502c3 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/DefaultSettingsRepositoryTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.settings.repository + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.db.ACTUAL_SCHEMA_VERSION +import de.gematik.ti.erp.app.db.TestDB +import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.AuthenticationPasswordEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.PasswordEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.PharmacySearchEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.ShippingContactEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.settings.model.SettingsData +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultSettingsRepositoryTest : TestDB() { + @get:Rule + val coroutineRule = CoroutineTestRule() + + lateinit var realm: Realm + + private lateinit var repo: DefaultSettingsRepository + + @Before + fun setUp() { + realm = Realm.open( + RealmConfiguration.Builder( + schema = setOf( + SettingsEntityV1::class, + PharmacySearchEntityV1::class, + PasswordEntityV1::class, + ShippingContactEntityV1::class, + PharmacySearchEntityV1::class, + AddressEntityV1::class + ) + ) + .schemaVersion(ACTUAL_SCHEMA_VERSION) + .directory(tempDBPath) + .build() + ).also { + it.writeBlocking { + copyToRealm(SettingsEntityV1()) + } + } + + repo = DefaultSettingsRepository( + dispatchers = StandardTestDispatcher(), + realm = realm + ) + } + + @Test + fun `general settings`() = runTest { + repo.general.first().also { + assertEquals(false, it.zoomEnabled) + assertEquals(false, it.mlKitAccepted) + assertEquals(false, it.userHasAcceptedInsecureDevice) + } + + repo.acceptInsecureDevice() + + repo.acceptUpdatedDataTerms(Instant.fromEpochSeconds(123456)) + + repo.saveZoomPreference(true) + repo.acceptMlKit() + + repo.general.first().also { + assertEquals(true, it.zoomEnabled) + assertEquals(true, it.mlKitAccepted) + assertEquals(true, it.userHasAcceptedInsecureDevice) + } + + repo.general.first().also { + assertEquals(true, it.zoomEnabled) + assertEquals(true, it.userHasAcceptedInsecureDevice) + } + } + + @Test + fun `pharmacy search`() = runTest { + repo.pharmacySearch.first().also { + assertEquals("", it.name) + assertEquals(false, it.locationEnabled) + assertEquals(false, it.deliveryService) + assertEquals(false, it.onlineService) + assertEquals(false, it.openNow) + } + + repo.savePharmacySearch( + SettingsData.PharmacySearch( + name = "Some Pharmacy", + locationEnabled = true, + deliveryService = true, + onlineService = false, + openNow = true + ) + ) + + repo.pharmacySearch.first().also { + assertEquals("Some Pharmacy", it.name) + assertEquals(true, it.locationEnabled) + assertEquals(true, it.deliveryService) + assertEquals(false, it.onlineService) + assertEquals(true, it.openNow) + } + } + + @Test + fun `authentication mode`() = runTest { + repo.authentication.first().also { + assertTrue { + it.methodIsUnspecified + } + } + + repo.enableDeviceSecurity() + + repo.authentication.first().also { + assertTrue { + it.methodIsDeviceSecurity + } + } + + repo.disableDeviceSecurity() + repo.setPassword(SettingsData.Authentication.Password("password")) + + repo.authentication.first().also { + assertTrue { + it.methodIsPassword + } + it.password?.let { password -> + assertEquals(false, password.isValid("Test123456")) + assertEquals(true, password.isValid("password")) + } + } + } + + @Test + fun `authentication mode set to both`() = runTest { + repo.setPassword(SettingsData.Authentication.Password("password")) + + realm.queryFirst()!!.also { + assertEquals(true, it.hash.isNotEmpty()) + assertEquals(true, it.salt.isNotEmpty()) + } + + repo.enableDeviceSecurity() + + repo.authentication.first().also { + assertTrue { + it.bothMethodsAvailable + } + } + realm.queryFirst()!!.also { + assertEquals(true, it.hash.isNotEmpty()) + assertEquals(true, it.salt.isNotEmpty()) + } + } + + @Test + fun `number of authentication failures`() = runTest { + repo.authentication.first().also { + assertEquals(0, it.failedAuthenticationAttempts) + } + + repo.incrementNumberOfAuthenticationFailures() + repo.incrementNumberOfAuthenticationFailures() + + repo.authentication.first().also { + assertEquals(2, it.failedAuthenticationAttempts) + } + + repo.resetNumberOfAuthenticationFailures() + + repo.authentication.first().also { + assertEquals(0, it.failedAuthenticationAttempts) + } + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt deleted file mode 100644 index 89ab681f..00000000 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.repository - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.db.TestDB -import de.gematik.ti.erp.app.db.ACTUAL_SCHEMA_VERSION -import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.PasswordEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.PharmacySearchEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.ShippingContactEntityV1 -import de.gematik.ti.erp.app.db.queryFirst -import de.gematik.ti.erp.app.settings.model.SettingsData -import io.realm.kotlin.Realm -import io.realm.kotlin.RealmConfiguration -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -@OptIn(ExperimentalCoroutinesApi::class) -class SettingsRepositoryTest : TestDB() { - @get:Rule - val coroutineRule = CoroutineTestRule() - - lateinit var realm: Realm - - private lateinit var repo: SettingsRepository - - @Before - fun setUp() { - realm = Realm.open( - RealmConfiguration.Builder( - schema = setOf( - SettingsEntityV1::class, - PharmacySearchEntityV1::class, - PasswordEntityV1::class, - ShippingContactEntityV1::class, - PharmacySearchEntityV1::class, - AddressEntityV1::class - ) - ) - .schemaVersion(ACTUAL_SCHEMA_VERSION) - .directory(tempDBPath) - .build() - ).also { - it.writeBlocking { - copyToRealm(SettingsEntityV1()) - } - } - - repo = SettingsRepository( - dispatchers = coroutineRule.dispatchers, - realm = realm - ) - } - - @Test - fun `general settings`() = runTest { - repo.general.first().also { - assertEquals(false, it.zoomEnabled) - assertEquals(false, it.mlKitAccepted) - assertEquals(false, it.userHasAcceptedInsecureDevice) - assertEquals(0, it.authenticationFails) - } - - repo.acceptInsecureDevice() - - repo.acceptUpdatedDataTerms(Instant.fromEpochSeconds(123456)) - - repo.incrementNumberOfAuthenticationFailures() - repo.incrementNumberOfAuthenticationFailures() - - repo.saveZoomPreference(true) - repo.acceptMlKit() - - repo.general.first().also { - assertEquals(true, it.zoomEnabled) - assertEquals(true, it.mlKitAccepted) - assertEquals(true, it.userHasAcceptedInsecureDevice) - assertEquals(2, it.authenticationFails) - } - - repo.resetNumberOfAuthenticationFailures() - repo.general.first().also { - assertEquals(true, it.zoomEnabled) - assertEquals(true, it.userHasAcceptedInsecureDevice) - assertEquals(0, it.authenticationFails) - } - } - - @Test - fun `pharmacy search`() = runTest { - repo.pharmacySearch.first().also { - assertEquals("", it.name) - assertEquals(false, it.locationEnabled) - assertEquals(false, it.deliveryService) - assertEquals(false, it.onlineService) - assertEquals(false, it.openNow) - } - - repo.savePharmacySearch( - SettingsData.PharmacySearch( - name = "Some Pharmacy", - locationEnabled = true, - deliveryService = true, - onlineService = false, - openNow = true - ) - ) - - repo.pharmacySearch.first().also { - assertEquals("Some Pharmacy", it.name) - assertEquals(true, it.locationEnabled) - assertEquals(true, it.deliveryService) - assertEquals(false, it.onlineService) - assertEquals(true, it.openNow) - } - } - - @Test - fun `authentication mode`() = runTest { - repo.authenticationMode.first().also { - assertTrue { - it is SettingsData.AuthenticationMode.Unspecified - } - } - - repo.saveAuthenticationMode( - SettingsData.AuthenticationMode.DeviceSecurity - ) - - repo.authenticationMode.first().also { - assertTrue { - it is SettingsData.AuthenticationMode.DeviceSecurity - } - } - - repo.saveAuthenticationMode( - SettingsData.AuthenticationMode.Password("Test123") - ) - - repo.authenticationMode.first().also { - assertTrue { - it is SettingsData.AuthenticationMode.Password - } - val password = it as SettingsData.AuthenticationMode.Password - - assertEquals(false, password.isValid("Test123456")) - assertEquals(true, password.isValid("Test123")) - } - } - - @Test - fun `authentication mode set to password - set other mode will reset stored credentials`() = runTest { - repo.saveAuthenticationMode( - SettingsData.AuthenticationMode.Password("Test123") - ) - - realm.queryFirst()!!.also { - assertEquals(true, it.hash.isNotEmpty()) - assertEquals(true, it.salt.isNotEmpty()) - } - - repo.saveAuthenticationMode( - SettingsData.AuthenticationMode.DeviceSecurity - ) - - realm.queryFirst()!!.also { - assertEquals(true, it.hash.isEmpty()) - assertEquals(true, it.salt.isEmpty()) - } - } -} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCaseTest.kt index 675c7744..0252576d 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCaseTest.kt @@ -1,24 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -30,7 +30,7 @@ class AcceptMLKitUseCaseTest { private lateinit var acceptMLKitUseCase: AcceptMLKitUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCaseTest.kt index 80246de5..ba881d46 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCaseTest.kt @@ -1,23 +1,24 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -30,7 +31,7 @@ class AllowScreenshotsUseCaseTest { private lateinit var allowScreenshotsUseCase: AllowScreenshotsUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/ChangeAnalyticsStateUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/ChangeAnalyticsStateUseCaseTest.kt index 8c593116..325f182a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/ChangeAnalyticsStateUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/ChangeAnalyticsStateUseCaseTest.kt @@ -1,25 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -31,7 +31,7 @@ class ChangeAnalyticsStateUseCaseTest { private lateinit var allowAnalyticsUseCase: ChangeAnalyticsStateUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCaseTest.kt index d2d286ce..56d71072 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -35,7 +36,7 @@ class GetCanStartToolTipsUseCaseTest { private lateinit var getCanStartToolTipsUseCase: GetCanStartToolTipsUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { @@ -56,7 +57,6 @@ class GetCanStartToolTipsUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false @@ -78,7 +78,6 @@ class GetCanStartToolTipsUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false @@ -100,7 +99,6 @@ class GetCanStartToolTipsUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false @@ -122,7 +120,6 @@ class GetCanStartToolTipsUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCaseTest.kt index e9196338..9aa49e30 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -35,7 +36,7 @@ class GetMLKitAcceptedUseCaseTest { private lateinit var getMLKitAcceptedUseCase: GetMLKitAcceptedUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { @@ -56,7 +57,6 @@ class GetMLKitAcceptedUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = true, trackingAllowed = false, screenShotsAllowed = false @@ -78,7 +78,6 @@ class GetMLKitAcceptedUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCaseTest.kt index 71775246..0a9f336f 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -34,7 +35,7 @@ class GetOnboardingSucceededUseCaseTest { private lateinit var getOnboardingSucceededUseCase: GetOnboardingSucceededUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { @@ -55,7 +56,6 @@ class GetOnboardingSucceededUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = true, trackingAllowed = false, screenShotsAllowed = false @@ -77,7 +77,6 @@ class GetOnboardingSucceededUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCaseTest.kt index e2db01da..987e8225 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -35,7 +36,7 @@ class GetScreenShotsAllowedUseCaseTest { private lateinit var getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { @@ -56,7 +57,6 @@ class GetScreenShotsAllowedUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = true, trackingAllowed = false, screenShotsAllowed = true @@ -78,7 +78,6 @@ class GetScreenShotsAllowedUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCaseTest.kt index 2895b6c6..17db5998 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCaseTest.kt @@ -1,24 +1,25 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ + package de.gematik.ti.erp.app.settings.usecase import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.repository.DefaultSettingsRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -35,7 +36,7 @@ class GetShowWelcomeDrawerUseCaseTest { private lateinit var getShowWelcomeDrawerUseCase: GetShowWelcomeDrawerUseCase @MockK(relaxed = true) - private lateinit var settingsRepository: SettingsRepository + private lateinit var settingsRepository: DefaultSettingsRepository @Before fun setup() { @@ -56,7 +57,6 @@ class GetShowWelcomeDrawerUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false @@ -78,7 +78,6 @@ class GetShowWelcomeDrawerUseCaseTest { zoomEnabled = false, userHasAcceptedInsecureDevice = false, userHasAcceptedIntegrityNotOk = false, - authenticationFails = 0, mlKitAccepted = false, trackingAllowed = false, screenShotsAllowed = false diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/utils/DMUtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/utils/DMUtilsTest.kt index fa79acac..60e8a6bb 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/utils/DMUtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/utils/DMUtilsTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.utils diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/AdapterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/AdapterTest.kt index 4da252a5..39bf86d3 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/AdapterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/AdapterTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CertUtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CertUtilsTest.kt index 52664146..2d0bbb31 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CertUtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CertUtilsTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/ClientCryptoTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/ClientCryptoTest.kt index 10d15fc1..353cc2e0 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/ClientCryptoTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/ClientCryptoTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau @@ -31,11 +31,11 @@ import okio.ByteString.Companion.decodeHex import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Test import java.security.KeyPairGenerator import java.security.interfaces.ECPublicKey import java.security.spec.ECGenParameterSpec import javax.crypto.spec.SecretKeySpec -import kotlin.test.Test import kotlin.test.assertContentEquals class ClientCryptoTest { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CryptoTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CryptoTest.kt index e4c22549..5638d564 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CryptoTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/CryptoTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt index 41709a04..0317b31b 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt @@ -1,29 +1,30 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau +import de.gematik.ti.erp.app.Requirement import org.bouncycastle.cert.ocsp.BasicOCSPResp import org.bouncycastle.cert.ocsp.OCSPResp import org.bouncycastle.util.encoders.Base64 import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import kotlin.test.Test +import org.junit.Test import kotlin.time.Duration.Companion.hours class OCSPUtilsTest { @@ -39,6 +40,12 @@ class OCSPUtilsTest { ocspResp.checkSignatureWith(TestCertificates.CA10.X509Certificate) } + @Requirement( + "A_21218#7", + sourceSpecification = "gemSpec_Krypt", + rationale = "Fail when there is no matching certificate.", + codeLines = 6 + ) @Test fun `valid ocsp response cert is valid within 12 hours`() { val ocspResp = OCSPResp(Base64.decode(TestCertificates.OCSP1.Base64)).responseObject as BasicOCSPResp @@ -55,6 +62,11 @@ class OCSPUtilsTest { ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.plus(13.hours)) } + @Requirement( + "A_21218#4", + sourceSpecification = "gemSpec_Krypt", + rationale = "Only if OCSP response greater than 12h available, we must request new ones." + ) @Test(expected = Exception::class) fun `valid ocsp response cert is invalid if current time is in the past - throws exception`() { val ocspResp = OCSPResp(Base64.decode(TestCertificates.OCSP1.Base64)).responseObject as BasicOCSPResp diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt index c67a77c8..6ad31237 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt @@ -1,29 +1,28 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("ktlint:max-line-length") +@file:Suppress("ktlint:max-line-length", "MayBeConst") package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList import kotlinx.datetime.Instant -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okio.ByteString.Companion.decodeBase64 import org.bouncycastle.cert.X509CertificateHolder @@ -58,7 +57,7 @@ object TestCertificates { const val SerialNumber = "347632017809591" - // FIXME second ca is ocsp response only; production ocsp uses same ca as vau/idp + // TODO second ca is ocsp response only; production ocsp uses same ca as vau/idp val JsonCertList = """ { "add_roots": [], diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/UtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/UtilsTest.kt index 8841433a..9f9e73cc 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/UtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/UtilsTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.app.vau diff --git a/common/src/commonTest/resources/communications_bundle_version_1_3.json b/common/src/commonTest/resources/communications_bundle_version_1_3.json new file mode 100644 index 00000000..d18b25e1 --- /dev/null +++ b/common/src/commonTest/resources/communications_bundle_version_1_3.json @@ -0,0 +1,227 @@ +{ + "id": "39b9ec0c-37ac-4ff6-948c-06bfeb199f74", + "type": "searchset", + "timestamp": "2024-08-15T07:04:49.562+00:00", + "resourceType": "Bundle", + "total": 3, + "entry": [ + { + "fullUrl": "https://erp-ref.zetral.erp.splitdns.ti-dienste.de/Communication/01ebc980-ae10-41f0-5a9f-c8ad61141a66", + "resource": { + "resourceType": "Communication", + "id": "01ebc980-ae10-41f0-5a9f-c8ad61141a66", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_Reply|1.3" + ] + }, + "basedOn": [ + { + "reference": "Task/160.000.226.545.733.51" + } + ], + "status": "unknown", + "recipient": [ + { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X110432693" + } + } + ], + "payload": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AvailabilityState", + "valueCoding": { + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_AvailabilityStatus", + "code": "30" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_SupplyOptionsType", + "extension": [ + { + "url": "onPremise", + "valueBoolean": true + }, + { + "url": "delivery", + "valueBoolean": false + }, + { + "url": "shipment", + "valueBoolean": false + } + ] + } + ], + "contentString": "Eisern" + } + ], + "sent": "2024-08-14T11:14:38.230+00:00", + "sender": { + "identifier": { + "value": "3-01.2.2023001.16.103", + "system": "https://gematik.de/fhir/sid/telematik-id" + } + }, + "received": "2024-08-14T11:14:46.000+00:00" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://erp-ref.zentral.erp.splitdns.ti-dienste.de/Communication/01ebc980-c555-9bf8-66b2-0d434e302916", + "resource": { + "resourceType": "Communication", + "id": "01ebc980-c555-9bf8-66b2-0d434e302916", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_Reply|1.3" + ] + }, + "basedOn": [ + { + "reference": "Task/160.000.226.545.733.51" + } + ], + "status": "unknown", + "recipient": [ + { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X110432693" + } + } + ], + "payload": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AvailabilityState", + "valueCoding": { + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_AvailabilityStatus", + "code": "30" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_SupplyOptionsType", + "extension": [ + { + "url": "onPremise", + "valueBoolean": true + }, + { + "url": "delivery", + "valueBoolean": false + }, + { + "url": "shipment", + "valueBoolean": false + } + ] + } + ], + "contentString": "Eisern" + } + ], + "sent": "2024-08-14T11:21:08.651+00:00", + "sender": { + "identifier": { + "value": "3-01.2.2023001.16.103", + "system": "https://gematik.de/fhir/sid/telematik-id" + } + }, + "received": "2024-08-14T11:21:15.000+00:00" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://erp-ref.zentral.erp.splitdns.ti-dienste.de/Communication/01ebc980-cb72-d730-762e-dd08075f568a", + "resource": { + "resourceType": "Communication", + "id": "01ebc980-cb72-d730-762e-dd08075f568a", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_Reply|1.3" + ] + }, + "basedOn": [ + { + "reference": "Task/160.000.226.545.733.51" + } + ], + "status": "unknown", + "recipient": [ + { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X110432693" + } + } + ], + "payload": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AvailabilityState", + "valueCoding": { + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_AvailabilityStatus", + "code": "30" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_SupplyOptionsType", + "extension": [ + { + "url": "onPremise", + "valueBoolean": true + }, + { + "url": "delivery", + "valueBoolean": false + }, + { + "url": "shipment", + "valueBoolean": false + } + ] + } + ], + "contentString": "Eisern" + } + ], + "sent": "2024-08-14T11:22:51.230+00:00", + "sender": { + "identifier": { + "value": "3-01.2.2023001.16.103", + "system": "https://gematik.de/fhir/sid/telematik-id" + } + }, + "received": "2024-08-14T11:28:44.000+00:00" + }, + "search": { + "mode": "match" + } + } + ], + "link": [ + { + "relation": "last", + "url": "https://erp-ref.zentral.erp.splitdns.ti-dienste.de/Communication?_sort=sent&_count=50&__offset=0" + }, + { + "relation": "first", + "url": "https://erp-ref.zentral.erp.splitdns.ti-dienste.de/Communication?_sort=sent&_count=50&__offset=0" + }, + { + "relation": "self", + "url": "https://erp-ref.zentral.erp.splitdns.ti-dienste.de/Communication?_sort=sent" + } + ] +} diff --git a/common/src/commonTest/resources/fhir/task_bundle_vers_1_3.json b/common/src/commonTest/resources/fhir/task_bundle_vers_1_3.json new file mode 100644 index 00000000..fe21f3a7 --- /dev/null +++ b/common/src/commonTest/resources/fhir/task_bundle_vers_1_3.json @@ -0,0 +1,200 @@ +{ + "resourceType": "Bundle", + "id": "f5ba6eaf-9052-42f6-ac4e-fadceed7293b", + "meta": { + "lastUpdated": "2020-03-01T07:02:37.836+00:00" + }, + "type": "collection", + "link": [ + { + "relation": "self", + "url": "https://erp.app.ti-dienste.de/Task/" + } + ], + "entry": [ + { + "fullUrl": "https://erp.app.ti-dienste.de/Task/160.123.456.789.123.58", + "resource": { + "resourceType": "Task", + "id": "160.123.456.789.123.58", + "meta": { + "versionId": "2", + "lastUpdated": "2020-02-18T10:05:05.038+00:00", + "source": "#AsYR9plLkvONJAiv", + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.3" + ] + }, + "identifier": [ + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.123.456.789.123.58" + }, + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode", + "value": "777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea" + } + ], + "intent": "order", + "status": "ready", + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_PrescriptionType", + "valueCoding": { + "code": "160", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_FlowType", + "display": "Muster 16 (Apothekenpflichtige Arzneimittel)" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate", + "valueDate": "2020-06-02" + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate", + "valueDate": "2020-04-01" + } + ], + "authoredOn": "2020-03-02T08:25:05+00:00", + "lastModified": "2020-03-02T08:45:05+00:00", + "performerType": [ + { + "coding": [ + { + "code": "urn:oid:1.2.276.0.76.4.54", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_OrganizationType", + "display": "Öffentliche Apotheke" + } + ] + } + ] + } + }, + { + "fullUrl": "https://erp.app.ti-dienste.de/Task/160.123.456.789.123.78", + "resource": { + "resourceType": "Task", + "id": "160.123.456.789.123.78", + "meta": { + "versionId": "2", + "lastUpdated": "2020-02-18T10:06:05.038+00:00", + "source": "#AsYR9plLkvONJAiv", + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.3" + ] + }, + "identifier": [ + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.123.456.789.123.78" + }, + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode", + "value": "777bea0e13cc9c42ceec14aec3ddee8402643dc2c6c699db115f58fe423607ea" + } + ], + "intent": "order", + "status": "ready", + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_PrescriptionType", + "valueCoding": { + "code": "160", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_FlowType", + "display": "Muster 16 (Apothekenpflichtige Arzneimittel)" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate", + "valueDate": "2020-06-02" + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate", + "valueDate": "2020-04-01" + } + ], + "authoredOn": "2020-03-02T08:25:05+00:00", + "lastModified": "2020-03-02T08:45:05+00:00", + "performerType": [ + { + "coding": [ + { + "code": "urn:oid:1.2.276.0.76.4.54", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_OrganizationType", + "display": "Öffentliche Apotheke" + } + ] + } + ] + } + }, + { + "fullUrl": "https://erp.app.ti-dienste.de/Task/160.123.456.789.123.61", + "resource": { + "resourceType": "Task", + "id": "160.123.456.789.123.61", + "meta": { + "versionId": "2", + "lastUpdated": "2020-02-18T10:05:05.038+00:00", + "source": "#AsYR9plLkvONJAiv", + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.3" + ] + }, + "identifier": [ + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.123.456.789.123.61" + }, + { + "use": "official", + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode", + "value": "777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607bl" + } + ], + "intent": "order", + "status": "in-progress", + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_PrescriptionType", + "valueCoding": { + "code": "160", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_FlowType", + "display": "Muster 16 (Apothekenpflichtige Arzneimittel)" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate", + "valueDate": "2020-06-02" + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate", + "valueDate": "2020-04-01" + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_LastMedicationDispense", + "valueInstant": "2020-04-01T16:37:17+01:00" + } + ], + "authoredOn": "2020-03-02T08:25:05+00:00", + "lastModified": "2020-03-02T08:45:05+00:00", + "performerType": [ + { + "coding": [ + { + "code": "urn:oid:1.2.276.0.76.4.54", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_OrganizationType", + "display": "Öffentliche Apotheke" + } + ] + } + ] + } + } + ] +} diff --git a/common/src/commonTest/resources/fhir/task_vers_1_3.json b/common/src/commonTest/resources/fhir/task_vers_1_3.json new file mode 100644 index 00000000..07a706e4 --- /dev/null +++ b/common/src/commonTest/resources/fhir/task_vers_1_3.json @@ -0,0 +1,61 @@ +{ +"resourceType": "Task", +"id": "160.123.456.789.123.61", +"meta": { +"versionId": "2", +"lastUpdated": "2020-02-18T10:05:05.038+00:00", +"source": "#AsYR9plLkvONJAiv", +"profile": [ +"https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.3" +] +}, +"identifier": [ +{ +"use": "official", +"system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", +"value": "160.123.456.789.123.61" +}, +{ +"use": "official", +"system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode", +"value": "777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607bl" +} +], +"intent": "order", +"status": "in-progress", +"extension": [ +{ +"url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_PrescriptionType", +"valueCoding": { +"code": "160", +"system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_FlowType", +"display": "Muster 16 (Apothekenpflichtige Arzneimittel)" +} +}, +{ +"url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate", +"valueDate": "2020-06-02" +}, +{ +"url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate", +"valueDate": "2020-04-01" +}, +{ +"url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_LastMedicationDispense", +"valueInstant": "2020-04-01T16:37:17+01:00" +} +], +"authoredOn": "2020-03-02T08:25:05+00:00", +"lastModified": "2020-03-02T08:45:05+00:00", +"performerType": [ +{ +"coding": [ +{ +"code": "urn:oid:1.2.276.0.76.4.54", +"system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_OrganizationType", +"display": "Öffentliche Apotheke" +} +] +} +] +} diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Kombipackung.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Kombipackung.json new file mode 100644 index 00000000..9deed8f6 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Kombipackung.json @@ -0,0 +1,177 @@ +{ + "resourceType": "Medication", + "id": "Medication-Kombipackung", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "Augentropfen", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pharmaceutical-product" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "373873005", + "display": "Pharmaceutical / biologic product (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "https://terminologieserver.bfarm.de/fhir/CodeSystem/arzneimittel-referenzdaten-pharmazeutisches-produkt", + "code": "01746517-1", + "display": "Augentropfen" + } + ] + }, + "ingredient": [ + { + "itemCodeableConcept": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/atc", + "code": "R01AC01", + "display": "Natriumcromoglicat" + } + ] + }, + "strength": { + "numerator": { + "value": 20, + "unit": "mg", + "system": "http://unitsofmeasure.org", + "code": "mg" + }, + "denominator": { + "value": 1, + "unit": "ml", + "system": "http://unitsofmeasure.org", + "code": "ml" + } + } + } + ] + }, + { + "resourceType": "Medication", + "id": "NasenSpray", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pharmaceutical-product" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "373873005", + "display": "Pharmaceutical / biologic product (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "https://terminologieserver.bfarm.de/fhir/CodeSystem/arzneimittel-referenzdaten-pharmazeutisches-produkt", + "code": "01746517-2", + "display": "Nasenspray, Lösung" + } + ] + }, + "ingredient": [ + { + "itemCodeableConcept": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/atc", + "code": "R01AC01", + "display": "Natriumcromoglicat" + } + ] + }, + "strength": { + "numerator": { + "value": 2.8, + "unit": "mg", + "system": "http://unitsofmeasure.org", + "code": "mg" + }, + "denominator": { + "value": 1, + "unit": "Sprühstoß", + "system": "http://unitsofmeasure.org", + "code": "1" + } + } + } + ] + } + ], + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "781405001", + "display": "Medicinal product package" + } + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension", + "valueBoolean": false + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/drug-category-extension", + "valueCoding": { + "system": "https://gematik.de/fhir/epa-medication/CodeSystem/epa-drug-category-cs", + "code": "00", + "display": "Arzneimittel oder in die Arzneimittelversorgung nach § 31 SGB V einbezogenes Produkt" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "1746517", + "display": "CROMO-RATIOPHARM Kombipackung" + } + ] + }, + "status": "active", + "form": { + "coding": [ + { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM", + "code": "KPG" + } + ], + "text": "Kombipackung" + }, + "ingredient": [ + { + "itemReference": { + "reference": "#NasenSpray" + } + }, + { + "itemReference": { + "reference": "#Augentropfen" + } + } + ], + "batch": { + "lotNumber": "56498416854" + } +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur-FD.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur-FD.json new file mode 100644 index 00000000..3f5397e8 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur-FD.json @@ -0,0 +1,232 @@ +{ + "resourceType": "Medication", + "id": "Medication-Rezeptur-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "MedicationHydrocortison-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "03424249", + "display": "Hydrocortison 1% Creme" + } + ], + "text": "Hydrocortison 1% Creme" + } + }, + { + "resourceType": "Medication", + "id": "MedicationDexpanthenol-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "16667195", + "display": "Dexpanthenol 5% Creme" + } + ], + "text": "Dexpanthenol 5% Creme" + } + } + ], + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/drug-category-extension", + "valueCoding": { + "system": "https://gematik.de/fhir/epa-medication/CodeSystem/epa-drug-category-cs", + "code": "00" + } + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-manufacturing-instructions-extension", + "valueString": "Bitte kühl zubereiten und lagern." + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-formulation-packaging-extension", + "valueString": "Tube" + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension", + "valueBoolean": false + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "1208954007", + "display": "Extemporaneous preparation (product)" + } + } + ], + "code": { + "text": "Hydrocortison-Dexpanthenol-Salbe" + }, + "form": { + "text": "Salbe" + }, + "amount": { + "numerator": { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-total-quantity-formulation-extension", + "valueString": "100" + } + ], + "value": 20, + "unit": "ml" + }, + "denominator": { + "value": 1 + } + }, + "ingredient": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-ingredient-darreichungsform-extension", + "valueString": "Salbe" + } + ], + "itemReference": { + "reference": "#MedicationHydrocortison-FD" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "unit": "g", + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + }, + "_code": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + "denominator": { + "value": 100, + "unit": "g", + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + }, + "_code": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + } + } + }, + { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-ingredient-darreichungsform-extension", + "valueString": "Salbe" + } + ], + "itemReference": { + "reference": "#MedicationDexpanthenol-FD" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "unit": "g", + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + }, + "_code": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + "denominator": { + "value": 100, + "unit": "g", + "_system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + }, + "_code": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur.json new file mode 100644 index 00000000..d39683a2 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/Medication-Medication-Rezeptur.json @@ -0,0 +1,161 @@ +{ + "resourceType": "Medication", + "id": "Medication-Rezeptur", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "MedicationHydrocortison", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "03424249", + "display": "Hydrocortison 1% Creme" + } + ] + }, + "batch": { + "lotNumber": "56498416854" + } + }, + { + "resourceType": "Medication", + "id": "MedicationDexpanthenol", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "16667195", + "display": "Dexpanthenol 5% Creme" + } + ] + }, + "batch": { + "lotNumber": "0132456" + } + } + ], + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/drug-category-extension", + "valueCoding": { + "system": "https://gematik.de/fhir/epa-medication/CodeSystem/epa-drug-category-cs", + "code": "00" + } + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "1208954007", + "display": "Extemporaneous preparation (product)" + } + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension", + "valueBoolean": false + } + ], + "code": { + "text": "Hydrocortison-Dexpanthenol-Salbe" + }, + "form": { + "coding": [ + { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM", + "code": "SAL" + } + ] + }, + "amount": { + "numerator": { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-total-quantity-formulation-extension", + "valueString": "100" + } + ], + "value": 20, + "unit": "ml" + }, + "denominator": { + "value": 1 + } + }, + "ingredient": [ + { + "itemReference": { + "reference": "#MedicationHydrocortison" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "system": "http://unitsofmeasure.org", + "code": "g" + }, + "denominator": { + "value": 100, + "system": "http://unitsofmeasure.org", + "code": "g" + } + } + }, + { + "itemReference": { + "reference": "#MedicationDexpanthenol" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "system": "http://unitsofmeasure.org", + "code": "g" + }, + "denominator": { + "value": 100, + "system": "http://unitsofmeasure.org", + "code": "g" + } + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_compounding_medication.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_compounding_medication.json new file mode 100644 index 00000000..9d69c33a --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_compounding_medication.json @@ -0,0 +1,149 @@ +{ + "resourceType": "Bundle", + "id": "KomplexMedicationDispenseBundle", + "type": "searchset", + "entry": [ + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/MedicationDispense/160.000.000.000.000.03", + "resource": { + "resourceType": "MedicationDispense", + "id": "160.000.000.000.000.03", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.4" + ] + }, + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.000.000.000.03" + } + ], + "status": "completed", + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X123456789" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-SMC-B-Testkarte-883110000095957" + } + } + } + ], + "whenHandedOver": "2025-09-06", + "medicationReference": { + "reference": "urn:uuid:627e0f0c-1e11-4985-901a-033bffd9ac67" + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:627e0f0c-1e11-4985-901a-033bffd9ac67", + "resource": { + "resourceType": "Medication", + "id": "627e0f0c-1e11-4985-901a-033bffd9ac67", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "code": "1208954007", + "system": "http://snomed.info/sct", + "display": "Extemporaneous preparation (product)", + "version": "http://snomed.info/sct/900000000000207008/version/20240201" + } + } + ], + "contained": [ + { + "resourceType": "Medication", + "id": "44509630-ea44-4fe6-a66c-9fe0dded85e1", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "code": "781405001", + "system": "http://snomed.info/sct", + "display": "Medicinal product package (product)", + "version": "http://snomed.info/sct/900000000000207008/version/20240201" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "03424249", + "display": "Hydrocortison 1% Creme" + } + ], + "text": "Hydrocortison 1% Creme" + } + }, + { + "resourceType": "Medication", + "id": "5c731ef9-fcce-46f8-80e2-d316a37057fe", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "code": "781405001", + "system": "http://snomed.info/sct", + "display": "Medicinal product package (product)", + "version": "http://snomed.info/sct/900000000000207008/version/20240201" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "16667195", + "display": "Dexpanthenol 5% Creme" + } + ], + "text": "Dexpanthenol 5% Creme" + } + } + ], + "ingredient": [ + { + "itemReference": { + "reference": "#44509630-ea44-4fe6-a66c-9fe0dded85e1" + } + }, + { + "itemReference": { + "reference": "#5c731ef9-fcce-46f8-80e2-d316a37057fe" + } + } + ] + }, + "search": { + "mode": "include" + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_simple_medication.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_simple_medication.json new file mode 100644 index 00000000..108bef4c --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_dispense_simple_medication.json @@ -0,0 +1,72 @@ +{ + "resourceType": "Bundle", + "id": "SimpleMedicationDispenseBundle", + "type": "searchset", + "entry": [ + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/MedicationDispense/160.000.000.000.000.01", + "resource": { + "resourceType": "MedicationDispense", + "id": "160.000.000.000.000.01", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.4" + ] + }, + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.000.000.000.01" + } + ], + "status": "completed", + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X123456789" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-SMC-B-Testkarte-883110000095957" + } + } + } + ], + "whenHandedOver": "2025-09-06", + "medicationReference": { + "reference": "urn:uuid:86ce7563-9819-4dfa-9944-d307f7cfec9b" + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:86ce7563-9819-4dfa-9944-d307f7cfec9b", + "resource": { + "resourceType": "Medication", + "id": "86ce7563-9819-4dfa-9944-d307f7cfec9b", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06313728" + } + ] + } + }, + "search": { + "mode": "include" + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_multiple_dispenses_simple_medications.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_multiple_dispenses_simple_medications.json new file mode 100644 index 00000000..f3bc21dd --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/bundle_multiple_dispenses_simple_medications.json @@ -0,0 +1,137 @@ +{ + "resourceType": "Bundle", + "id": "MultipleMedicationDispenseBundle", + "type": "searchset", + "entry": [ + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/MedicationDispense/160.000.000.000.000.01", + "resource": { + "resourceType": "MedicationDispense", + "id": "160.000.000.000.000.01", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.4" + ] + }, + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.000.000.000.01" + } + ], + "status": "completed", + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X123456789" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-SMC-B-Testkarte-883110000095957" + } + } + } + ], + "whenHandedOver": "2025-09-06", + "medicationReference": { + "reference": "urn:uuid:86ce7563-9819-4dfa-9944-d307f7cfec9b" + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:86ce7563-9819-4dfa-9944-d307f7cfec9b", + "resource": { + "resourceType": "Medication", + "id": "86ce7563-9819-4dfa-9944-d307f7cfec9b", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06313728" + } + ] + } + }, + "search": { + "mode": "include" + } + }, + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/MedicationDispense/160.000.000.000.000.02", + "resource": { + "resourceType": "MedicationDispense", + "id": "160.000.000.000.000.02", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.4" + ] + }, + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.000.000.000.02" + } + ], + "status": "completed", + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X123456789" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-SMC-B-Testkarte-883110000095957" + } + } + } + ], + "whenHandedOver": "2025-09-06", + "medicationReference": { + "reference": "urn:uuid:56c61db7-0a94-4b7b-832a-b8ac3752035d" + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "urn:uuid:56c61db7-0a94-4b7b-832a-b8ac3752035d", + "resource": { + "resourceType": "Medication", + "id": "56c61db7-0a94-4b7b-832a-b8ac3752035d", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06313728" + } + ] + } + }, + "search": { + "mode": "include" + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/complexMedication.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/complexMedication.json new file mode 100644 index 00000000..98184232 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/complexMedication.json @@ -0,0 +1,200 @@ +{ + "resourceType": "Medication", + "id": "Medication-Rezeptur-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/drug-category-extension", + "valueCoding": { + "system": "https://gematik.de/fhir/epa-medication/CodeSystem/epa-drug-category-cs", + "code": "00" + } + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-manufacturing-instructions-extension", + "valueString": "Bitte kühl zubereiten und lagern." + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-formulation-packaging-extension", + "valueString": "Tube" + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension", + "valueBoolean": false + }, + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "1208954007", + "display": "Extemporaneous preparation (product)" + } + } + ], + "code": { + "text": "Hydrocortison-Dexpanthenol-Salbe" + }, + "form": { + "text": "Salbe" + }, + "amount": { + "numerator": { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-total-quantity-formulation-extension", + "valueString": "100" + } + ], + "value": 20, + "unit": "ml" + }, + "denominator": { + "value": 1 + } + }, + "contained": [ + { + "resourceType": "Medication", + "id": "MedicationHydrocortison-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "03424249", + "display": "Hydrocortison 1% Creme" + } + ], + "text": "Hydrocortison 1% Creme" + } + }, + { + "resourceType": "Medication", + "id": "MedicationDexpanthenol-FD", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pzn-ingredient" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20240201", + "code": "781405001", + "display": "Medicinal product package (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "16667195", + "display": "Dexpanthenol 5% Creme" + } + ], + "text": "Dexpanthenol 5% Creme" + } + } + ], + "ingredient": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-ingredient-darreichungsform-extension", + "valueString": "Salbe" + } + ], + "itemReference": { + "reference": "#MedicationHydrocortison-FD" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "unit": "g", + "system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + "denominator": { + "value": 100, + "unit": "g", + "system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + } + } + }, + { + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-ingredient-darreichungsform-extension", + "valueString": "Salbe" + } + ], + "itemReference": { + "reference": "#MedicationDexpanthenol-FD" + }, + "isActive": true, + "strength": { + "numerator": { + "value": 50, + "unit": "g", + "system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + }, + "denominator": { + "value": 100, + "unit": "g", + "system": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + } + } + } + ] +} diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/pharmaceuticalProduct.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/pharmaceuticalProduct.json new file mode 100644 index 00000000..807f3e06 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/pharmaceuticalProduct.json @@ -0,0 +1,55 @@ +{ + "resourceType": "Medication", + "id": "Augentropfen", + "meta": { + "profile": [ + "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-pharmaceutical-product" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/epa-medication-type-extension", + "valueCoding": { + "system": "http://snomed.info/sct", + "code": "373873005", + "display": "Pharmaceutical / biologic product (product)" + } + } + ], + "code": { + "coding": [ + { + "system": "https://terminologieserver.bfarm.de/fhir/CodeSystem/arzneimittel-referenzdaten-pharmazeutisches-produkt", + "code": "01746517-1", + "display": "Augentropfen" + } + ] + }, + "ingredient": [ + { + "itemCodeableConcept": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/bfarm/atc", + "code": "R01AC01", + "display": "Natriumcromoglicat" + } + ] + }, + "strength": { + "numerator": { + "value": 20, + "unit": "mg", + "system": "http://unitsofmeasure.org", + "code": "mg" + }, + "denominator": { + "value": 1, + "unit": "ml", + "system": "http://unitsofmeasure.org", + "code": "ml" + } + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/workflow_version_1_4/resource_simple_medication.json b/common/src/commonTest/resources/fhir/workflow_version_1_4/resource_simple_medication.json new file mode 100644 index 00000000..29765f41 --- /dev/null +++ b/common/src/commonTest/resources/fhir/workflow_version_1_4/resource_simple_medication.json @@ -0,0 +1,27 @@ +{ + "resourceType": "Medication", + "id": "18cac59e-2e3a-4546-983b-6c5f9d375cd9", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Medication|1.4" + ] + }, + "extension": [ + { + "url": "https://gematik.de/fhir/epa-medication/StructureDefinition/medication-id-vaccine-extension", + "valueBoolean": false + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "82082973" + } + ] + }, + "batch": { + "lotNumber": "1419556306", + "expirationDate": "2024-12-25T02:35:18+00:00" + } +} \ No newline at end of file diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/Constants.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/Constants.kt deleted file mode 100644 index 19855955..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/Constants.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -object Constants { - object IDP { - val serviceUri = BuildKonfig.IDP_SERVICE_URI - } - - object ERP { - val serviceUri = BuildKonfig.BASE_SERVICE_URI - val apiKey = BuildKonfig.ERP_API_KEY - } - - val userAgent = BuildKonfig.USER_AGENT - val trustAnchor = BuildKonfig.APP_TRUST_ANCHOR_BASE64 -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt deleted file mode 100644 index 9133d2cd..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/SecureRandomProvider.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -import java.security.SecureRandom - -actual fun secureRandomInstance(): SecureRandom = SecureRandom() diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt deleted file mode 100644 index 60b384d2..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.di - -import de.gematik.ti.erp.app.db.appSchemas -import de.gematik.ti.erp.app.db.openRealmWith -import de.gematik.ti.erp.app.secureRandomInstance -import de.gematik.ti.erp.app.utils.cleanupDbFiles -import io.realm.kotlin.Realm -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindings.Scope -import org.kodein.di.bindings.ScopeCloseable -import org.kodein.di.scoped -import org.kodein.di.singleton - -private const val PassphraseSizeInBytes = 64 - -@JvmInline -value class ScopedRealm(val realm: Realm) : ScopeCloseable { - override fun close() { - realm.close() - cleanupDbFiles().start() - } -} - -fun realmModule(scope: Scope) = DI.Module("Realm Module") { - bind { - scoped(scope).singleton { - ScopedRealm( - openRealmWith( - schemas = appSchemas, - configuration = { - it.encryptionKey(generatePassPhrase()) - it.directory(System.getProperty("java.io.tmpdir")) - } - ) - ) - } - } -} - -private fun generatePassPhrase(): ByteArray { - return ByteArray(PassphraseSizeInBytes).apply { - secureRandomInstance().nextBytes(this) - } -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt deleted file mode 100644 index acfb5138..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.di - -import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList -import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList -import de.gematik.ti.erp.app.vau.interceptor.DefaultCryptoConfig -import de.gematik.ti.erp.app.vau.interceptor.VauChannelInterceptor -import de.gematik.ti.erp.app.vau.repository.VauLocalDataSource -import de.gematik.ti.erp.app.vau.repository.VauRemoteDataSource -import de.gematik.ti.erp.app.vau.repository.VauRepository -import de.gematik.ti.erp.app.vau.usecase.TrustedTruststore -import de.gematik.ti.erp.app.vau.usecase.TruststoreConfig -import de.gematik.ti.erp.app.vau.usecase.TruststoreTimeSourceProvider -import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import org.bouncycastle.cert.X509CertificateHolder -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindInstance -import org.kodein.di.bindings.Scope -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import kotlin.time.Duration - -fun vauModule(scope: Scope) = DI.Module("VAU Module") { - bindInstance { - TruststoreConfig { - if (BuildKonfig.INTERNAL) { - BuildKonfig.APP_TRUST_ANCHOR_BASE64_TU - } else { - BuildKonfig.APP_TRUST_ANCHOR_BASE64_PU - } - } - } - bind { scoped(scope).singleton { VauRemoteDataSource(instance()) } } - bind { scoped(scope).singleton { VauLocalDataSource(instance().realm) } } - bind { scoped(scope).singleton { VauRepository(instance(), instance(), instance()) } } - bind { scoped(scope).singleton { DefaultCryptoConfig() } } - bind { scoped(scope).singleton { VauChannelInterceptor(instance(), instance(), instance()) } } - bind { scoped(scope).singleton { TruststoreUseCase(instance(), instance(), instance(), instance()) } } - bind { scoped(scope).singleton { { Clock.System.now() } } } - bind { - scoped(scope).singleton { - { untrustedOCSPList: UntrustedOCSPList, - untrustedCertList: UntrustedCertList, - trustAnchor: X509CertificateHolder, - ocspResponseMaxAge: Duration, - timestamp: Instant -> - TrustedTruststore.create( - untrustedOCSPList = untrustedOCSPList, - untrustedCertList = untrustedCertList, - trustAnchor = trustAnchor, - ocspResponseMaxAge = ocspResponseMaxAge, - timestamp = timestamp - ) - } - } - } -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt deleted file mode 100644 index 778b901d..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpCryptoProvider.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.usecase - -import java.security.KeyStore -import java.security.Signature - -actual class IdpCryptoProvider { - actual fun keyStoreInstance(): KeyStore = - KeyStore.getInstance(KeyStore.getDefaultType()) - actual fun signatureInstance(algorithm: String): Signature = - Signature.getInstance(algorithm, KeyStore.getDefaultType()) -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt deleted file mode 100644 index d9a1c747..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpDeviceInfoProvider.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.usecase - -actual class IdpDeviceInfoProvider { - actual val deviceName: String = "" - actual val manufacturer: String = "" - actual val productName: String = "" - actual val model: String = "" - actual val operatingSystem: String = "" - actual val operatingSystemVersion: String = "" -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt deleted file mode 100644 index 9c90490e..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpPreferenceProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.usecase - -actual class IdpPreferenceProvider { - actual var externalAuthenticationPreferences: ExternalAuthenticationPreferences - get() = ExternalAuthenticationPreferences() - set(_) {} -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt deleted file mode 100644 index 134c2668..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils - -import java.io.File - -fun cleanupDbFiles(): Thread = Thread { - val rootDir = System.getProperty("java.io.tmpdir") - File("$rootDir/default.realm").delete() - File("$rootDir/default.realm.lock").delete() - File("$rootDir/default.realm.management").deleteRecursively() -} diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt deleted file mode 100644 index a5e7b184..00000000 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.vau.interceptor - -import de.gematik.ti.erp.app.Constants -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.vau.VauChannelSpec -import de.gematik.ti.erp.app.vau.VauCryptoConfig -import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase -import kotlinx.coroutines.runBlocking -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Interceptor -import okhttp3.Response -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.IOException -import java.net.HttpURLConnection.HTTP_FORBIDDEN -import java.net.HttpURLConnection.HTTP_UNAUTHORIZED -import java.security.Provider -import java.security.SecureRandom - -class DefaultCryptoConfig : VauCryptoConfig { - override val provider: Provider by lazy { BouncyCastleProvider() } - override val random: SecureRandom - get() = SecureRandom() -} - -/** - * Wrapper for exceptions originating from the [VauChannelInterceptor]. - */ -class VauException(e: Exception) : IOException(e) - -class VauChannelInterceptor( - private val truststore: TruststoreUseCase, - private val cryptoConfig: VauCryptoConfig, - private val dispatchProvider: DispatchProvider -) : Interceptor { - // `gemSpec_Krypt A_20175` - private var previousUserAlias = "0" - private val baseUrl = Constants.ERP.serviceUri.toHttpUrl() - - override fun intercept(chain: Interceptor.Chain): Response { - try { - val encryptedRequest = runBlocking(dispatchProvider.io) { - truststore.withValidVauPublicKey { publicKey -> - VauChannelSpec.V1.encryptHttpRequest( - chain.request(), - previousUserAlias, - publicKey, - baseUrl, - cryptoConfig - ) - } - } - - // outer response - val encryptedResponse = chain.proceed(encryptedRequest.first) - - return if (!encryptedResponse.isSuccessful) { - // e.g. 401 -> user pseudonym unknown -> reset to zero - if (encryptedResponse.code == HTTP_UNAUTHORIZED || encryptedResponse.code == HTTP_FORBIDDEN) { - previousUserAlias = "0" - } - - encryptedResponse - } else { - val (decryptedResponse, userpseudonym) = VauChannelSpec.V1.decryptHttpResponse( - encryptedResponse, - encryptedRequest.first, - encryptedRequest.second, - cryptoConfig - ) - - userpseudonym?.let { - previousUserAlias = it - } - - decryptedResponse - } - } catch (e: Exception) { - previousUserAlias = "0" - - // wrap all exceptions - throw VauException(e) - } - } -} diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 47caa4b6..9dfcb920 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -1,5 +1,8 @@ + + ConstructorParameterNaming:PrescriptionDetailController.kt$PrescriptionDetailController$private val _prescription: MutableStateFlow<UiState<PrescriptionData.Prescription>> = MutableStateFlow(UiState.Loading()) + ClassNaming:SchemaTest.kt$RealmA_V1 : RealmObject ClassNaming:SchemaTest.kt$RealmA_V2 : RealmObject @@ -324,9 +327,6 @@ NestedBlockDepth:EntityUtils.kt$private suspend fun SequenceScope<Deleteable>.flatten( currentObject: Cascading, currentDepth: Int, maxDepth: Int ) NestedBlockDepth:LicenceRule.kt$LicenceRule$override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) MemberNameEqualsClassName:AppDependenciesPlugin.kt$AppDependenciesPlugin.Dependencies.Lottie$const val lottie = "com.airbnb.android:lottie-compose:$lottieVersion" - MemberNameEqualsClassName:Navigation.kt$Route$val route = arguments.fold(Uri.Builder().path(path)) { uri, param -> uri.appendQueryParameter(param.name, "{${param.name}}") }.build().toString() - NestedBlockDepth:EntityUtils.kt$private suspend fun SequenceScope<Deleteable>.flatten( currentObject: Cascading, currentDepth: Int, maxDepth: Int ) - NestedBlockDepth:LicenceRule.kt$LicenceRule$override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) NestedBlockDepth:Utils.kt$fun ByteArray.contains(other: ByteArray): Boolean ReturnCount:BiometricPrompt.kt$@Requirement( "A_21582", sourceSpecification = "gemSpec_IDP_Frontend", rationale = "Selection of the best available authentication option on the device." ) private fun bestSecureOption(biometricManager: BiometricManager): Int ReturnCount:TestResource.kt$TestResource$fun getParameter(parameterEnum: ParameterEnum): Any? @@ -339,7 +339,7 @@ SwallowedException:OCSPUtils.kt$e: Exception SwallowedException:SecureMessagingTest.kt$SecureMessagingTest$e: Exception SwallowedException:TruststoreUseCase.kt$TruststoreUseCase$e: Exception - ThrowsCount:IdpUseCase.kt$IdpUseCase$private suspend fun loadAccessToken( refresh: Boolean = false, profileId: ProfileIdentifier, scope: IdpScope, singleSignOnTokenScope: suspend () -> IdpData.SingleSignOnTokenScope?, decryptedAccessToken: suspend () -> String?, invalidateDecryptedAccessToken: suspend () -> Unit, invalidateSingleSignOnTokenRetainingScope: suspend () -> Unit, saveDecryptedAccessToken: suspend (decryptedAccessToken: String) -> Unit ): String + ThrowsCount:IdpUseCase.kt$IdpUseCase$private suspend fun loadAccessToken( refresh: Boolean = false, profileId: ProfileIdentifier, scope: IdpScope, singleSignOnTokenScope: suspend () -> IdpData.SingleSignOnTokenScope?, decryptedAccessToken: suspend () -> String?, invalidateDecryptedAccessToken: suspend () -> Unit, invalidateSingleSignOnTokenRetainingScope: suspend () -> Unit, saveDecryptedAccessToken: suspend (decryptedAccessToken: String) -> Unit ): String TooGenericExceptionCaught:AuthenticationUseCase.kt$AuthenticationUseCase$e: Exception TooGenericExceptionCaught:CertUtils.kt$e: Exception TooGenericExceptionCaught:DebugLoadingButton.kt$e: Exception diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 70fb85ae..45709c2c 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -144,11 +144,11 @@ complexity: TooManyFunctions: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] - thresholdInFiles: 15 - thresholdInClasses: 15 - thresholdInInterfaces: 15 - thresholdInObjects: 15 - thresholdInEnums: 15 + thresholdInFiles: 25 + thresholdInClasses: 25 + thresholdInInterfaces: 25 + thresholdInObjects: 25 + thresholdInEnums: 25 ignoreDeprecated: false ignorePrivate: true ignoreOverridden: false @@ -558,7 +558,7 @@ style: active: true MaxLineLength: active: false - maxLineLength: 120 + maxLineLength: 160 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: true diff --git a/desktop/AppxManifest.xml b/desktop/AppxManifest.xml deleted file mode 100644 index 461ddcfa..00000000 --- a/desktop/AppxManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - E-Rezept - gematik GmbH - E-Rezept Desktop - Square44x44Logo.png - - - - - - - - - - - - - - - - diff --git a/desktop/E-Rezept-Dev.ico b/desktop/E-Rezept-Dev.ico deleted file mode 100644 index cc42e18c..00000000 Binary files a/desktop/E-Rezept-Dev.ico and /dev/null differ diff --git a/desktop/E-Rezept.ico b/desktop/E-Rezept.ico deleted file mode 100644 index b37b5bdf..00000000 Binary files a/desktop/E-Rezept.ico and /dev/null differ diff --git a/desktop/Square150x150Logo.png b/desktop/Square150x150Logo.png deleted file mode 100644 index 1f396d7e..00000000 Binary files a/desktop/Square150x150Logo.png and /dev/null differ diff --git a/desktop/Square44x44Logo.png b/desktop/Square44x44Logo.png deleted file mode 100644 index 40ea25c7..00000000 Binary files a/desktop/Square44x44Logo.png and /dev/null differ diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts deleted file mode 100644 index ad0df706..00000000 --- a/desktop/build.gradle.kts +++ /dev/null @@ -1,157 +0,0 @@ -import de.gematik.ti.erp.Dependencies -import de.gematik.ti.erp.inject -import de.gematik.ti.erp.networkSecurityConfigGen.AndroidNetworkConfigGeneratorTask -import de.gematik.ti.erp.stringResGen.AndroidStringResourceGeneratorTask -import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import java.util.Locale - -plugins { - kotlin("multiplatform") - kotlin("plugin.serialization") - id("org.jetbrains.compose") - id("de.gematik.ti.erp.resgen") - id("de.gematik.ti.erp.dependencies") -} - -afterEvaluate { - tasks.getByName("createDistributable").apply { - doLast { - // remove build env dirs - val configFile = fileTree(outputs.files.asPath) { include("**/E-Rezept-Desktop.cfg") }.asFileTree.singleFile - configFile.writeText( - configFile.useLines { lines -> - lines - .filter { !it.contains(rootProject.rootDir.path, ignoreCase = true) } - .joinToString(System.lineSeparator()) - } - ) - } - } -} - -tasks.withType { - resourceFiles = listOf( - stringResPath("values/strings.xml") to Locale.GERMAN, - stringResPath("values/strings_desktop.xml") to Locale.GERMAN, - stringResPath("values/strings_kbv_codes.xml") to Locale.GERMAN, - stringResPath("values-en/strings.xml") to Locale.ENGLISH, - stringResPath("values-tr/strings.xml") to Locale.forLanguageTag("tr") - ) - outputPath = file(project.projectDir.path + "/src/jvmMain/kotlin") - packagePath = "de.gematik.ti.erp.app.common.strings" -} - -fun stringResPath(name: String): String { - return rootDir.path + "/android/src/main/res/" + name -} - -tasks.withType { - resourceFile = file(networkConfigPath("network_security_config.xml")) - outputPath = file(project.projectDir.path + "/src/jvmMain/kotlin") - packagePath = "de.gematik.ti.erp.app.common.pinning" -} - -fun networkConfigPath(name: String): String { - return rootDir.path + "/android/src/main/res/xml/" + name -} - -kotlin { - jvm { - compilations.all { - kotlinOptions.jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - } - withJava() - } - sourceSets { - val jvmMain by getting { - dependencies { - implementation(project(":common")) - - // TODO move to multiplatform lib for nfc - implementation("de.gematik.ti.erp.app:smartcard-wrapper:1.0") - - implementation(compose.desktop.currentOs) - implementation(compose.desktop.common) - - implementation(compose.materialIconsExtended) - - inject { - androidX { - compileOnly(multiplatformPaging) - } - coroutines { - implementation(coroutinesSwing) - } - dateTime { - implementation(datetime) - } - dependencyInjection { - compileOnly(kodeinCompose) - } - dataMatrix { - implementation(zxing) - } - logging { - implementation(napier) - implementation(slf4jNoOp) - } - serialization { - implementation(fhir) - implementation(kotlinXJson) - } - crypto { - implementation(jose4j) - implementation(bouncycastleBcprov) - implementation(bouncycastleBcpkix) - } - network { - implementation(retrofit) - implementation(retrofit2KotlinXSerialization) - implementation(okhttp3) - implementation(okhttpLogging) - // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). - // Can be removed when Retrofit releases a new version >2.9.0. - implementation(okio) - } - database { - compileOnly(realm) - } - } - } - } - val jvmTest by getting - } -} - -compose.desktop { - application { - mainClass = "de.gematik.ti.erp.app.MainKt" - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "E-Rezept-Desktop" - packageVersion = "1.0.0" - modules("java.smartcardio") - - macOS { - iconFile.set(rootProject.file("resources/icon/ERezept.icns")) - } - windows { - iconFile.set( - project.file( - when { - (project.property("buildkonfig.flavor") as? String) - ?.endsWith("Internal") == true -> "E-Rezept-Dev.ico" - - else -> "E-Rezept.ico" - } - ) - ) - menuGroup = "gematik" - } - linux { - iconFile.set(rootProject.file("resources/icon/ERezept.png")) - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/androidx/paging/compose/LazyPagingItems.kt b/desktop/src/jvmMain/kotlin/androidx/paging/compose/LazyPagingItems.kt deleted file mode 100644 index 1e0a8ca1..00000000 --- a/desktop/src/jvmMain/kotlin/androidx/paging/compose/LazyPagingItems.kt +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -// -// Source: https://android.googlesource.com/platform/frameworks/support/+/androidx-main/paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt -// Changes: -// - remove android specific code for PagingPlaceholderKey -// - -package androidx.paging.compose - -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.paging.CombinedLoadStates -import androidx.paging.DifferCallback -import androidx.paging.ItemSnapshotList -import androidx.paging.LoadState -import androidx.paging.LoadStates -import androidx.paging.NullPaddedList -import androidx.paging.PagingData -import androidx.paging.PagingDataDiffer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.collectLatest - -/** - * The class responsible for accessing the data from a [Flow] of [PagingData]. - * In order to obtain an instance of [LazyPagingItems] use the [collectAsLazyPagingItems] extension - * method of [Flow] with [PagingData]. - * This instance can be used by the [items] and [itemsIndexed] methods inside [LazyListScope] to - * display data received from the [Flow] of [PagingData]. - * - * @param T the type of value used by [PagingData]. - */ -public class LazyPagingItems internal constructor( - /** - * the [Flow] object which contains a stream of [PagingData] elements. - */ - private val flow: Flow> -) { - private val mainDispatcher = Dispatchers.Main - - /** - * Contains the immutable [ItemSnapshotList] of currently presented items, including any - * placeholders if they are enabled. - * Note that similarly to [peek] accessing the items in a list will not trigger any loads. - * Use [get] to achieve such behavior. - */ - var itemSnapshotList by mutableStateOf( - ItemSnapshotList(0, 0, emptyList()) - ) - private set - - /** - * The number of items which can be accessed. - */ - val itemCount: Int get() = itemSnapshotList.size - - private val differCallback: DifferCallback = object : DifferCallback { - override fun onChanged(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - - override fun onInserted(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - - override fun onRemoved(position: Int, count: Int) { - if (count > 0) { - updateItemSnapshotList() - } - } - } - - private val pagingDataDiffer = object : PagingDataDiffer( - differCallback = differCallback, - mainDispatcher = mainDispatcher - ) { - override suspend fun presentNewList( - previousList: NullPaddedList, - newList: NullPaddedList, - lastAccessedIndex: Int, - onListPresentable: () -> Unit - ): Int? { - onListPresentable() - updateItemSnapshotList() - return null - } - } - - private fun updateItemSnapshotList() { - itemSnapshotList = pagingDataDiffer.snapshot() - } - - /** - * Returns the presented item at the specified position, notifying Paging of the item access to - * trigger any loads necessary to fulfill prefetchDistance. - * - * @see peek - */ - operator fun get(index: Int): T? { - pagingDataDiffer[index] // this registers the value load - return itemSnapshotList[index] - } - - /** - * Returns the presented item at the specified position, without notifying Paging of the item - * access that would normally trigger page loads. - * - * @param index Index of the presented item to return, including placeholders. - * @return The presented item at position [index], `null` if it is a placeholder - */ - fun peek(index: Int): T? { - return itemSnapshotList[index] - } - - /** - * Retry any failed load requests that would result in a [LoadState.Error] update to this - * [LazyPagingItems]. - * - * Unlike [refresh], this does not invalidate [PagingSource], it only retries failed loads - * within the same generation of [PagingData]. - * - * [LoadState.Error] can be generated from two types of load requests: - * * [PagingSource.load] returning [PagingSource.LoadResult.Error] - * * [RemoteMediator.load] returning [RemoteMediator.MediatorResult.Error] - */ - fun retry() { - pagingDataDiffer.retry() - } - - /** - * Refresh the data presented by this [LazyPagingItems]. - * - * [refresh] triggers the creation of a new [PagingData] with a new instance of [PagingSource] - * to represent an updated snapshot of the backing dataset. If a [RemoteMediator] is set, - * calling [refresh] will also trigger a call to [RemoteMediator.load] with [LoadType] [REFRESH] - * to allow [RemoteMediator] to check for updates to the dataset backing [PagingSource]. - * - * Note: This API is intended for UI-driven refresh signals, such as swipe-to-refresh. - * Invalidation due repository-layer signals, such as DB-updates, should instead use - * [PagingSource.invalidate]. - * - * @see PagingSource.invalidate - */ - fun refresh() { - pagingDataDiffer.refresh() - } - - /** - * A [CombinedLoadStates] object which represents the current loading state. - */ - public var loadState: CombinedLoadStates by mutableStateOf( - CombinedLoadStates( - refresh = InitialLoadStates.refresh, - prepend = InitialLoadStates.prepend, - append = InitialLoadStates.append, - source = InitialLoadStates - ) - ) - private set - - internal suspend fun collectLoadState() { - pagingDataDiffer.loadStateFlow.collect { - loadState = it - } - } - - internal suspend fun collectPagingData() { - flow.collectLatest { - pagingDataDiffer.collectFrom(it) - } - } -} - -private val IncompleteLoadState = LoadState.NotLoading(false) -private val InitialLoadStates = LoadStates( - IncompleteLoadState, - IncompleteLoadState, - IncompleteLoadState -) - -/** - * Collects values from this [Flow] of [PagingData] and represents them inside a [LazyPagingItems] - * instance. The [LazyPagingItems] instance can be used by the [items] and [itemsIndexed] methods - * from [LazyListScope] in order to display the data obtained from a [Flow] of [PagingData]. - * - * @sample androidx.paging.compose.samples.PagingBackendSample - */ -@Composable -public fun Flow>.collectAsLazyPagingItems(): LazyPagingItems { - val lazyPagingItems = remember(this) { LazyPagingItems(this) } - - LaunchedEffect(lazyPagingItems) { - lazyPagingItems.collectPagingData() - } - LaunchedEffect(lazyPagingItems) { - lazyPagingItems.collectLoadState() - } - - return lazyPagingItems -} - -/** - * Adds the [LazyPagingItems] and their content to the scope. The range from 0 (inclusive) to - * [LazyPagingItems.itemCount] (exclusive) always represents the full range of presentable items, - * because every event from [PagingDataDiffer] will trigger a recomposition. - * - * @sample androidx.paging.compose.samples.ItemsDemo - * - * @param items the items received from a [Flow] of [PagingData]. - * @param key a factory of stable and unique keys representing the item. Using the same key - * for multiple items in the list is not allowed. Type of the key should be saveable - * via Bundle on Android. If null is passed the position in the list will represent the key. - * When you specify the key the scroll position will be maintained based on the key, which - * means if you add/remove items before the current visible item the item with the given key - * will be kept as the first visible one. - * @param itemContent the content displayed by a single item. In case the item is `null`, the - * [itemContent] method should handle the logic of displaying a placeholder instead of the main - * content displayed by an item which is not `null`. - */ -public fun LazyListScope.items( - items: LazyPagingItems, - key: ((item: T) -> Any)? = null, - itemContent: @Composable LazyItemScope.(value: T?) -> Unit -) { - items( - count = items.itemCount, - key = if (key == null) null else { index -> - val item = items.peek(index) - if (item == null) { - PagingPlaceholderKey(index) - } else { - key(item) - } - } - ) { index -> - itemContent(items[index]) - } -} - -/** - * Adds the [LazyPagingItems] and their content to the scope where the content of an item is - * aware of its local index. The range from 0 (inclusive) to [LazyPagingItems.itemCount] (exclusive) - * always represents the full range of presentable items, because every event from - * [PagingDataDiffer] will trigger a recomposition. - * - * @sample androidx.paging.compose.samples.ItemsIndexedDemo - * - * @param items the items received from a [Flow] of [PagingData]. - * @param key a factory of stable and unique keys representing the item. Using the same key - * for multiple items in the list is not allowed. Type of the key should be saveable - * via Bundle on Android. If null is passed the position in the list will represent the key. - * When you specify the key the scroll position will be maintained based on the key, which - * means if you add/remove items before the current visible item the item with the given key - * will be kept as the first visible one. - * @param itemContent the content displayed by a single item. In case the item is `null`, the - * [itemContent] method should handle the logic of displaying a placeholder instead of the main - * content displayed by an item which is not `null`. - */ -public fun LazyListScope.itemsIndexed( - items: LazyPagingItems, - key: ((index: Int, item: T) -> Any)? = null, - itemContent: @Composable LazyItemScope.(index: Int, value: T?) -> Unit -) { - items( - count = items.itemCount, - key = if (key == null) null else { index -> - val item = items.peek(index) - if (item == null) { - PagingPlaceholderKey(index) - } else { - key(index, item) - } - } - ) { index -> - itemContent(index, items[index]) - } -} - -private data class PagingPlaceholderKey(private val index: Int) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt deleted file mode 100644 index fcb7d2ab..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository -import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.supervisorScope - -class DownloadUseCase( - private val prescriptionRepository: DesktopPrescriptionRepository, - private val communicationRepository: DesktopCommunicationRepository -) { - suspend fun update() = - supervisorScope { - awaitAll( - async { - prescriptionRepository.download() - }, - async { - communicationRepository.download() - } - ).find { it.isFailure } ?: Result.success(Unit) - }.onFailure { - prescriptionRepository.invalidate() - communicationRepository.invalidate() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/JWSConverterFactory.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/JWSConverterFactory.kt deleted file mode 100644 index b6bd513e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/JWSConverterFactory.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -import de.gematik.ti.erp.app.idp.api.models.JWSKey -import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey -import de.gematik.ti.erp.app.idp.repository.JWSDiscoveryDocument -import okhttp3.ResponseBody -import org.jose4j.jwk.JsonWebKey -import org.jose4j.jwk.PublicJsonWebKey -import org.jose4j.jws.JsonWebSignature -import org.jose4j.jwx.JsonWebStructure -import retrofit2.Converter -import retrofit2.Retrofit -import java.lang.reflect.Type - -class JWSConverterFactory : Converter.Factory() { - override fun responseBodyConverter( - type: Type, - annotations: Array, - retrofit: Retrofit - ): Converter? = - when (type) { - JWSDiscoveryDocument::class.javaObjectType -> JWSDiscoveryDocumentConverter() - JWSKey::class.javaObjectType -> JWSKeyConverter() - JWSPublicKey::class.javaObjectType -> JWSPublicKeyConverter() - else -> null - } -} - -class JWSDiscoveryDocumentConverter : Converter { - override fun convert(value: ResponseBody): JWSDiscoveryDocument { - return JWSDiscoveryDocument( - JsonWebStructure.fromCompactSerialization( - value.string() - ) as JsonWebSignature - ) - } -} - -class JWSKeyConverter : Converter { - override fun convert(value: ResponseBody): JWSKey { - return JWSKey( - JsonWebKey.Factory.newJwk(value.string()) - ) - } -} - -class JWSPublicKeyConverter : Converter { - override fun convert(value: ResponseBody): JWSPublicKey { - return JWSPublicKey( - PublicJsonWebKey.Factory.newPublicJwk(value.string()) - ) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt deleted file mode 100644 index e1183445..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app - -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.BugReport -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.ReusableContent -import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.loadImageBitmap -import androidx.compose.ui.res.useResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.MenuBar -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.WindowState -import androidx.compose.ui.window.singleWindowApplication -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.SpacerTiny -import de.gematik.ti.erp.app.common.theme.DesktopAppTheme -import de.gematik.ti.erp.app.communication.di.communicationModule -import de.gematik.ti.erp.app.di.realmModule -import de.gematik.ti.erp.app.di.vauModule -import de.gematik.ti.erp.app.fhir.FhirMapper -import de.gematik.ti.erp.app.idp.di.idpModule -import de.gematik.ti.erp.app.main.ui.MainNavigation -import de.gematik.ti.erp.app.main.ui.MainScreen -import de.gematik.ti.erp.app.main.ui.MainScreenViewModel -import de.gematik.ti.erp.app.navigation.ui.rememberNavigation -import de.gematik.ti.erp.app.network.di.networkModule -import de.gematik.ti.erp.app.prescription.di.prescriptionModule -import de.gematik.ti.erp.app.protocol.di.protocolModule -import de.gematik.ti.erp.app.utils.cleanupDbFiles -import io.github.aakira.napier.DebugAntilog -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindSingleton -import org.kodein.di.bindings.UnboundedScope -import org.kodein.di.compose.rememberInstance -import org.kodein.di.compose.subDI -import org.kodein.di.compose.withDI -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import java.awt.event.MouseWheelListener -import java.util.logging.ConsoleHandler -import java.util.logging.Handler -import java.util.logging.Level -import java.util.logging.LogRecord -import java.util.logging.SimpleFormatter -import javax.swing.UIManager - -val BCProvider = BouncyCastleProvider() - -val applicationScope = UnboundedScope() - -@Composable -fun rememberScope(): UnboundedScope { - val scope = remember { UnboundedScope() } - DisposableEffect(Unit) { - onDispose { - scope.close() - } - } - return scope -} - -@Stable -class LogHandler : Handler() { - init { - formatter = SimpleFormatter() - } - - private val buffer = MutableStateFlow>(emptyList()) - - @Stable - val log: StateFlow> = buffer - - override fun publish(record: LogRecord) { - if (isLoggable(record)) { - buffer.update { - it + formatter.format(record) - } - } - } - - override fun flush() { - } - - override fun close() { - } -} - -val di = DI { - bindSingleton { object : DispatchProvider {} } -} - -fun main() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) - } catch (_: Exception) { - } - - val logHandler by lazy { LogHandler() } - if (BuildKonfig.INTERNAL) { - Napier.base( - DebugAntilog( - handler = listOf( - ConsoleHandler().apply { - level = Level.ALL - formatter = SimpleFormatter() - }, - logHandler - ) - ) - ) - } - - singleWindowApplication( - icon = BitmapPainter( - useResource( - if (BuildKonfig.INTERNAL) "images/erp_logo_dev.webp" else "images/erp_logo.webp", - ::loadImageBitmap - ) - ), - title = "E-Rezept-Desktop", - state = WindowState(width = 1200.dp, height = 800.dp) - ) { - val systemScale = LocalDensity.current.density - val zoomRange = (systemScale / 1.5f)..(systemScale * 1.5f) - cleanupDbFiles().start() - Runtime.getRuntime().addShutdownHook(cleanupDbFiles()) - - App { - MenuBar { - val uriHandler = LocalUriHandler.current - val infoText = App.strings.desktopMenuInfo() - val dataText = App.strings.desktopMenuData() - val dataLink = App.strings.desktopDataLink() - val termsText = App.strings.desktopMenuTerms() - val termsLink = App.strings.desktopTermsLink() - Menu(infoText) { - Item(dataText) { - uriHandler.openUri(dataLink) - } - Item(termsText) { - uriHandler.openUri(termsLink) - } - } - } - - val systemDarkMode = isSystemInDarkTheme() - - // close the app scope linked to OkHttps thread pools - DisposableEffect(Unit) { - onDispose { - applicationScope.close() - } - } - - withDI(di) { - val resourceScope = rememberScope() - - subDI(diBuilder = { - import(vauModule(resourceScope)) - import(networkModule) - bind { scoped(resourceScope).singleton { FhirMapper(instance(), instance()) } } - importAll(prescriptionModule(resourceScope)) - importAll(communicationModule(resourceScope)) - importAll(protocolModule(resourceScope)) - bind { scoped(resourceScope).singleton { DownloadUseCase(instance(), instance()) } } - bindSingleton { - MainScreenViewModel( - zoomRange = zoomRange, - defaultZoom = systemScale, - defaultDarkMode = systemDarkMode - ) - } - import(realmModule(resourceScope)) - import(idpModule(resourceScope)) - }) { - val navigation = rememberNavigation(MainNavigation.Welcome) - - val mainViewModel by rememberInstance() - val mainState by produceState(mainViewModel.defaultState) { - mainViewModel.screenState().collect { - value = it - } - } - - DisposableEffect(Unit) { - val l = MouseWheelListener { e -> - if (e.isControlDown) { - mainViewModel.onZoom(if (e.wheelRotation < 0) 0.1f else -0.1f) - } - } - window.addMouseWheelListener(l) - onDispose { - window.removeMouseWheelListener(l) - } - } - - ReusableContent(mainState.zoom) { - CompositionLocalProvider( - LocalDensity provides Density(density = mainState.zoom, fontScale = 1f) - ) { - DesktopAppTheme(darkTheme = mainState.darkMode) { - if (BuildKonfig.INTERNAL) { - Box { - MainScreen(mainViewModel, navigation) - - var showLoggingWindow by remember { mutableStateOf(false) } - Row( - modifier = Modifier.padding(8.dp).align(Alignment.BottomEnd), - verticalAlignment = Alignment.CenterVertically - ) { - Text(BuildKonfig.BUILD_FLAVOR, style = MaterialTheme.typography.caption) - SpacerTiny() - IconButton( - onClick = { showLoggingWindow = true } - ) { - Icon(Icons.Rounded.BugReport, null, tint = Color.Red) - } - } - - if (showLoggingWindow) { - LoggingWindow( - logHandler, - onCloseRequest = { showLoggingWindow = false } - ) - } - } - } else { - MainScreen(mainViewModel, navigation) - } - } - } - } - } - } - } - } -} - -@Composable -private fun LoggingWindow( - logHandler: LogHandler, - onCloseRequest: () -> Unit -) { - Window( - onCloseRequest = onCloseRequest - ) { - val logs by logHandler.log.collectAsState() - - val clipboardManager = LocalClipboardManager.current - - Column { - Button( - onClick = { - clipboardManager.setText(AnnotatedString(logs.joinToString("\n\n"))) - } - ) { - Text("Copy All") - } - - if (logs.isNotEmpty()) { - SelectionContainer(Modifier.fillMaxSize()) { - val state = rememberLazyListState() - Row { - LazyColumn( - Modifier.weight(1f), - state = state - ) { - items(items = logs) { log -> - Text(log) - } - } - VerticalScrollbar(rememberScrollbarAdapter(state)) - } - } - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt deleted file mode 100644 index 639b8414..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.api - -import org.hl7.fhir.r4.model.Bundle -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Query - -interface ErpService { - @GET("Task/{id}") - suspend fun getTaskWithKBVBundle(@Path("id") id: String): Response - - @POST("Task/{id}/\$abort") - suspend fun deleteTask(@Path("id") id: String): Response - - @GET("Task") - suspend fun getAllTasks(): Response - - @GET("AuditEvent") - suspend fun getAllAuditEvents( - @Query("_sort") sort: String = "-date", - @Query("_count") count: Int? = null, - @Query("__offset") offset: Int? = null - ): Response - - @GET("MedicationDispense") - suspend fun getAllMedicationDispenses(): Response - - @GET("Communication") - suspend fun getAllCommunications(): Response -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/cardwall/AuthenticationUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/cardwall/AuthenticationUseCase.kt deleted file mode 100644 index 78395ead..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/cardwall/AuthenticationUseCase.kt +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall - -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.card.model.command.ResponseException -import de.gematik.ti.erp.app.card.model.command.ResponseStatus -import de.gematik.ti.erp.app.card.model.exchange.getRandom -import de.gematik.ti.erp.app.card.model.exchange.retrieveCertificate -import de.gematik.ti.erp.app.card.model.exchange.signChallenge -import de.gematik.ti.erp.app.card.model.exchange.verifyPin -import de.gematik.ti.erp.app.cardwall.model.nfc.exchange.establishTrustedChannel -import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.nfc.model.card.NfcCardChannel -import de.gematik.ti.erp.app.nfc.model.card.NfcCardSecureChannel -import io.github.aakira.napier.Napier -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.consume -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import java.io.IOException -import kotlin.coroutines.cancellation.CancellationException - -@Requirement( - "GS-A_4368#6", - "GS-A_4367#7", - sourceSpecification = "gemSpec_Krypt", - rationale = "Seed length is defined with 256 Bits" - // TODO Update this req. when using the health card for random number generation is also implemented for Android. -) -private const val SEED_LENGTH = 256 - -enum class AuthenticationState { - None, - - // initial state - AuthenticationFlowInitialized, - - HealthCardCommunicationChannelReady, - HealthCardCommunicationTrustedChannelEstablished, - HealthCardCommunicationCertificateLoaded, - HealthCardCommunicationFinished, - - IDPCommunicationFinished, - - // final state - AuthenticationFlowFinished, - - // nfc failure - HealthCardCommunicationInterrupted, - - // health card failure states - HealthCardCardAccessNumberWrong, - HealthCardPin2RetriesLeft, - HealthCardPin1RetryLeft, - HealthCardBlocked, - - // IDP failure states - IDPCommunicationFailed, - - // secure element failure - SecureElementCryptographyFailed; - - fun isFailure() = - when (this) { - HealthCardCommunicationInterrupted, - HealthCardCardAccessNumberWrong, - HealthCardPin2RetriesLeft, - HealthCardPin1RetryLeft, - HealthCardBlocked, - IDPCommunicationFailed, - SecureElementCryptographyFailed -> true - else -> false - } - - fun isInProgress() = - when (this) { - AuthenticationFlowInitialized, - HealthCardCommunicationChannelReady, - HealthCardCommunicationTrustedChannelEstablished, - HealthCardCommunicationCertificateLoaded, - HealthCardCommunicationFinished, - IDPCommunicationFinished - -> true - else -> false - } - - fun isNotInProgress() = !isInProgress() - - fun isFinal() = this == AuthenticationFlowFinished - fun isReady() = this == None -} - -class AuthenticationUseCase( - private val idpUseCase: IdpUseCase -) { - @Requirement( - "GS-A_4367#2", - "GS-A_4368#1", - sourceSpecification = "gemSpec_Krypt", - rationale = "Random numbers are generated using the RNG of the health card." + - "This generator fulfills BSI-TR-03116#3.4 PTG.2 required by gemSpec_COS#14.9.5.1" - ) - fun authenticateWithHealthCard( - can: String, - pin: String, - cardChannel: Flow - ) = flow { - var retry: Boolean - do { - retry = false - authenticationFlowWithHealthCard( - can, - pin, - cardChannel - ) - .onEach { Napier.d("AuthenticationState: $it") } - .catch { cause -> - Napier.e("authenticationFlowWithHealthCard failed", cause) - handleException(cause).let { - emit(it) - - delay(1000) - retry = it == AuthenticationState.HealthCardCommunicationInterrupted - } - } - .collect { - emit(it) - } - } while (retry) - } - - @Requirement( - "GS-A_4367#3", - "GS-A_4368#2", - sourceSpecification = "gemSpec_Krypt", - rationale = "Random numbers are generated using the RNG of the health card." + - "This generator fulfills BSI-TR-03116#3.4 PTG.2 required by gemSpec_COS#14.9.5.1" - ) - private fun authenticationFlowWithHealthCard( - can: String, - pin: String, - cardChannel: Flow - ) = channelFlow { - send(AuthenticationState.AuthenticationFlowInitialized) - - cardChannel.first().use { nfcChannel -> - send(AuthenticationState.HealthCardCommunicationChannelReady) - - val healthCardCertificateChannel = Channel() - val signChannel = Channel() - val responseChannel = Channel() - - // - // + - IDP communication --------- + ------------- + -- + -------- + - // / ^ | ^ \ - // - start flow - + Health card cert Sign challenge + - end flow - - // \ | v | / - // + - Health card communication - + ------------- + -- + -------- + - // - - joinAll( - launch { - try { - idpUseCase.authenticationFlowWithHealthCard( - { - healthCardCertificateChannel.consume { receive() } - }, - { - signChannel.send(it) - signChannel.close() - responseChannel.consume { receive() } - } - ) - send(AuthenticationState.IDPCommunicationFinished) - } catch (e: Exception) { - handleAsyncExceptions(e, AuthenticationExceptionKind.IDPCommunicationFailed) - } - }, - launch { - try { - healthCardCommunication( - nfcChannel, - healthCardCertificateChannel, - signChannel, - responseChannel, - can = can, - pin = pin - ) - } catch (e: Exception) { - handleAsyncExceptions(e, AuthenticationExceptionKind.HealthCardCommunicationFailed) - } - } - ) - } - - send(AuthenticationState.AuthenticationFlowFinished) - } - - @Requirement( - "GS-A_4367#4", - "GS-A_4368#3", - sourceSpecification = "gemSpec_Krypt", - rationale = "Random numbers are generated using the RNG of the health card." + - "This generator fulfills BSI-TR-03116#3.4 PTG.2 required by gemSpec_COS#14.9.5.1" - ) - private suspend fun ProducerScope.healthCardCommunication( - channel: NfcCardChannel, - healthCardCertificateChannel: Channel, - signChannel: Channel, // `signChannel` is required to be closed by its caller - responseChannel: Channel, - can: String, - pin: String - ) { - _seed.value = channel.getRandom(SEED_LENGTH) - - val paceKey = channel.establishTrustedChannel(can) - - val secChannel = NfcCardSecureChannel( - channel.isExtendedLengthSupported, - channel.card, - paceKey - ) - send(AuthenticationState.HealthCardCommunicationTrustedChannelEstablished) - - healthCardCertificateChannel.send(secChannel.retrieveCertificate()) - send(AuthenticationState.HealthCardCommunicationCertificateLoaded) - - when (secChannel.verifyPin(pin)) { - ResponseStatus.SUCCESS -> { - signChannel.consumeEach { - responseChannel.send( - secChannel.signChallenge(it) - ) - } - } - ResponseStatus.WRONG_SECRET_WARNING_COUNT_02 -> - throw AuthenticationException(AuthenticationExceptionKind.HealthCardPin2RetriesLeft) - ResponseStatus.WRONG_SECRET_WARNING_COUNT_01 -> - throw AuthenticationException(AuthenticationExceptionKind.HealthCardPin1RetryLeft) - else -> { - throw AuthenticationException(AuthenticationExceptionKind.HealthCardBlocked) - } - } - - send(AuthenticationState.HealthCardCommunicationFinished) - } - - private fun handleException(e: Throwable): AuthenticationState = - when (e) { - is CancellationException -> throw e - is AuthenticationException -> { - when (e.kind) { - AuthenticationExceptionKind.IDPCommunicationFailed -> - AuthenticationState.IDPCommunicationFailed - - AuthenticationExceptionKind.HealthCardBlocked -> - AuthenticationState.HealthCardBlocked - AuthenticationExceptionKind.HealthCardPin1RetryLeft -> - AuthenticationState.HealthCardPin1RetryLeft - AuthenticationExceptionKind.HealthCardPin2RetriesLeft -> - AuthenticationState.HealthCardPin2RetriesLeft - AuthenticationExceptionKind.HealthCardCommunicationFailed -> - AuthenticationState.HealthCardCommunicationInterrupted - AuthenticationExceptionKind.SecureElementFailure -> - AuthenticationState.SecureElementCryptographyFailed - } - } - is ResponseException -> { - when (e.responseStatus) { - ResponseStatus.AUTHENTICATION_FAILURE -> AuthenticationState.HealthCardCardAccessNumberWrong - else -> AuthenticationState.HealthCardCommunicationInterrupted - } - } - is IOException -> { - Napier.e("IO Exception / NFC TAG was lost", e) - AuthenticationState.HealthCardCommunicationInterrupted - } - else -> { - Napier.e("Unknown exception", e) - // soft fail - AuthenticationState.HealthCardCommunicationInterrupted - } - } - - private fun handleAsyncExceptions(e: Throwable, kind: AuthenticationExceptionKind) { - if (e.suppressed.isNotEmpty()) { - throw e.suppressed.first() - } else { - Napier.e("async exception", e) - when (e) { - is CancellationException, - is AuthenticationException, - is ResponseException -> throw e - else -> throw AuthenticationException(kind) - } - } - } - - private enum class AuthenticationExceptionKind { - IDPCommunicationFailed, - - HealthCardBlocked, - HealthCardPin1RetryLeft, - HealthCardPin2RetriesLeft, - HealthCardCommunicationFailed, - - SecureElementFailure, - } - - private class AuthenticationException : IllegalStateException { - var kind: AuthenticationExceptionKind - private set - - constructor(kind: AuthenticationExceptionKind, cause: Throwable) : super(kind.name, cause) { - this.kind = kind - } - - constructor(kind: AuthenticationExceptionKind) : super(kind.name) { - this.kind = kind - } - } - companion object { - private val _seed = MutableStateFlow(byteArrayOf()) - val seed: StateFlow = _seed.asStateFlow() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/App.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/App.kt deleted file mode 100644 index 462d49e2..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/App.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import de.gematik.ti.erp.app.common.strings.LocalStrings -import de.gematik.ti.erp.app.common.strings.Strings -import de.gematik.ti.erp.app.common.strings.getStrings -import java.util.Locale - -@Composable -fun App(locale: Locale = Locale.getDefault(), content: @Composable () -> Unit) { - CompositionLocalProvider( - LocalStrings provides getStrings(locale), - content = content - ) -} - -object App { - val strings: Strings - @Composable - get() = LocalStrings.current -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/ClosableScaffold.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/ClosableScaffold.kt deleted file mode 100644 index 92354a3b..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/ClosableScaffold.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults - -@Composable -fun ClosablePopupScaffold( - onClose: () -> Unit, - content: @Composable (PaddingValues) -> Unit -) = - Scaffold( - topBar = { - Box(Modifier.fillMaxWidth()) { - IconButton( - onClick = onClose, - modifier = Modifier.padding(PaddingDefaults.Small).size(28.dp).align(Alignment.TopEnd) - ) { - Icon(Icons.Filled.Cancel, null, tint = AppTheme.colors.neutral400) - } - } - }, - backgroundColor = MaterialTheme.colors.surface, - content = content - ) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Common.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Common.kt deleted file mode 100644 index d856f609..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Common.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import de.gematik.ti.erp.app.common.theme.PaddingDefaults - -@Composable -fun SpacerLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.Large)) - -@Composable -fun SpacerMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.Medium)) - -@Composable -fun SpacerSmall() = - Spacer(modifier = Modifier.size(PaddingDefaults.Small)) - -@Composable -fun SpacerTiny() = - Spacer(modifier = Modifier.size(PaddingDefaults.Tiny)) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Dialog.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Dialog.kt deleted file mode 100644 index c3ec4a49..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Dialog.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntRect -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupPositionProvider -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults - -@Composable -fun Dialog( - title: String, - subtitle: String? = null, - confirmButton: @Composable () -> Unit, - dismissButton: (@Composable () -> Unit)? = null, - onDismissRequest: (() -> Unit)? = null -) { - Popup( - popupPositionProvider = object : PopupPositionProvider { - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize - ): IntOffset { - return IntOffset.Zero - } - } - ) { - Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.33f))) { - Surface( - elevation = 24.dp, - shape = RoundedCornerShape(8.dp), - modifier = Modifier.align(Alignment.Center) - ) { - Box { - Column( - Modifier.padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (onDismissRequest != null) { - SpacerMedium() - } - Text( - title, - style = MaterialTheme.typography.h6, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = PaddingDefaults.Large) - ) - subtitle?.let { - SpacerSmall() - Text(subtitle, style = AppTheme.typography.body1l, textAlign = TextAlign.Center) - } - SpacerMedium() - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - modifier = Modifier.wrapContentWidth() - ) { - dismissButton?.invoke() - confirmButton() - } - } - onDismissRequest?.let { - IconButton( - onClick = onDismissRequest, - modifier = Modifier.align(Alignment.TopEnd) - ) { - Icon(Icons.Filled.Cancel, null, tint = AppTheme.colors.neutral400) - } - } - } - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Divider.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Divider.kt deleted file mode 100644 index 1c837a74..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Divider.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.theme.AppTheme - -@Composable -fun HorizontalDivider() { - Box(Modifier.fillMaxWidth().height(1.dp).background(AppTheme.colors.neutral300)) -} - -@Composable -fun VerticalDivider() { - Box(Modifier.fillMaxHeight().width(1.dp).background(AppTheme.colors.neutral300)) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Hints.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Hints.kt deleted file mode 100644 index 298dca9f..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Hints.kt +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.absoluteOffset -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.contentColorFor -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.theme.AppTheme -import kotlinx.coroutines.delay -import java.util.Locale - -@Immutable -data class HintCardProperties( - val shape: Shape, - val backgroundColor: Color, - val contentColor: Color?, - val border: BorderStroke?, - val elevation: Dp -) - -object HintCardDefaults { - @Composable - fun properties( - shape: Shape = RoundedCornerShape(8.dp), - backgroundColor: Color = MaterialTheme.colors.surface, - contentColor: Color? = null, - border: BorderStroke = BorderStroke(0.5.dp, AppTheme.colors.neutral300), - elevation: Dp = 2.dp - ) = HintCardProperties( - shape = shape, - backgroundColor = backgroundColor, - contentColor = contentColor, - border = border, - elevation = elevation - ) - - @Composable - fun flatProperties( - shape: Shape = RoundedCornerShape(8.dp), - backgroundColor: Color = MaterialTheme.colors.surface, - contentColor: Color? = null - ) = HintCardProperties( - shape = shape, - backgroundColor = backgroundColor, - contentColor = contentColor, - border = null, - elevation = 0.dp - ) -} - -@Composable -fun HintCard( - modifier: Modifier = Modifier, - properties: HintCardProperties = HintCardDefaults.properties(), - image: @Composable RowScope.(innerPadding: PaddingValues) -> Unit, - title: (@Composable () -> Unit)?, - body: @Composable () -> Unit, - action: (@Composable ColumnScope.() -> Unit)? = null, - close: (@Composable (innerPadding: PaddingValues) -> Unit)? = null -) { - Card( - modifier = modifier, - shape = properties.shape, - backgroundColor = properties.backgroundColor, - contentColor = properties.contentColor ?: contentColorFor(properties.backgroundColor), - border = properties.border, - elevation = properties.elevation - ) { - if (properties.contentColor != null) { - MaterialTheme( - colors = MaterialTheme.colors.copy( - primary = properties.contentColor - ), - content = { HintCardInnerLayout(image, title, body, action, close) } - ) - } else { - HintCardInnerLayout(image, title, body, action, close) - } - } -} - -@Composable -private fun HintCardInnerLayout( - image: @Composable RowScope.(innerPadding: PaddingValues) -> Unit, - title: (@Composable () -> Unit)?, - body: @Composable () -> Unit, - action: (@Composable ColumnScope.() -> Unit)? = null, - close: (@Composable (innerPadding: PaddingValues) -> Unit)? = null -) { - val padding = 16.dp - val innerPaddingLeft = PaddingValues(start = padding, top = padding, bottom = padding) - val innerPaddingRight = PaddingValues(end = padding, top = padding, bottom = padding) - - Row( - modifier = Modifier - .graphicsLayer { - clip = false - } - ) { - image(innerPaddingLeft) - - Column( - modifier = Modifier - .padding(start = padding, bottom = padding) - .weight(1.0f) - .align(Alignment.CenterVertically) - .graphicsLayer { - clip = false - } - ) { - val noTitleModifier = if (title == null) { - Modifier.padding(top = padding) - } else { - Modifier - } - if (title != null) { - Row(horizontalArrangement = Arrangement.spacedBy(padding)) { - Box( - modifier = Modifier - .weight(1.0f) - .padding(top = padding, end = padding) - ) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.subtitle1 - ) { - title() - } - } - if (close != null) { - close(innerPaddingRight) - } - } - SpacerTiny() - } - Column( - modifier = Modifier - .padding(end = padding) - .then(noTitleModifier) - .graphicsLayer { - clip = false - } - ) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.body2 - ) { - body() - } - if (action != null) { - SpacerTiny() - action() - } - } - } - if (close != null && title == null) { - close(innerPaddingRight) - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun AnimatedHintCard( - modifier: Modifier = Modifier, - onTransitionEnd: suspend (Boolean) -> Unit, - initiallyVisible: MutableState = rememberSaveable { mutableStateOf(true) }, - visibleState: MutableTransitionState = remember { MutableTransitionState(initiallyVisible.value) }, - enterDelay: Int = 300, - enter: EnterTransition = fadeIn() + expandVertically(), - exit: ExitTransition = shrinkVertically() + fadeOut(), - properties: HintCardProperties = HintCardDefaults.properties(), - image: @Composable RowScope.(innerPadding: PaddingValues) -> Unit, - title: (@Composable () -> Unit)?, - body: @Composable () -> Unit, - action: (@Composable ColumnScope.() -> Unit)? = null, - close: (@Composable (innerPadding: PaddingValues) -> Unit)? = { - HintCloseButton(innerPadding = it) { - visibleState.targetState = false - } - } -) { - LaunchedEffect(visibleState.currentState) { - if (visibleState.currentState != initiallyVisible.value) { - onTransitionEnd(visibleState.currentState) - initiallyVisible.value = true - } - } - - LaunchedEffect(Unit) { - delay(enterDelay.toLong()) - visibleState.targetState = true - } - - AnimatedVisibility( - visibleState = visibleState, - enter = enter, - exit = exit - ) { - HintCard( - modifier = modifier, - properties = properties, - image = image, - title = title, - body = body, - action = action, - close = close - ) - } -} - -@Composable -fun RowScope.HintLargeImage( - painter: Painter, - contentDescription: String? = null, - innerPadding: PaddingValues -) { - val lDs = LocalLayoutDirection.current - val padding = PaddingValues( - top = innerPadding.calculateTopPadding(), - start = innerPadding.calculateStartPadding(lDs) - ) - - Image( - painter, - contentDescription, - modifier = Modifier - .padding(padding) - .requiredWidth(80.dp) - .align(Alignment.Bottom) - ) -} - -@Composable -fun RowScope.HintSmallImage( - painter: Painter, - contentDescription: String? = null, - innerPadding: PaddingValues -) { - Image( - painter, - contentDescription, - modifier = Modifier - .padding(innerPadding) - .size(80.dp) - .align(Alignment.Top) - ) -} - -@Composable -fun HintActionButton( - text: String, - enabled: Boolean = true, - onClick: () -> Unit -) { - Button( - modifier = Modifier.padding(top = 4.dp), - onClick = onClick, - elevation = ButtonDefaults.elevation( - defaultElevation = 8.dp, - pressedElevation = 16.dp, - disabledElevation = 2.dp - ), - enabled = enabled, - shape = RoundedCornerShape(8.dp) - ) { - Text(text.uppercase(Locale.getDefault())) - } -} - -@Composable -fun HintTextActionButton( - text: String, - enabled: Boolean = true, - onClick: () -> Unit -) { - val offset = ButtonDefaults.TextButtonContentPadding.calculateLeftPadding(LocalLayoutDirection.current) - - TextButton( - modifier = Modifier.absoluteOffset(x = -offset), - onClick = onClick, - enabled = enabled, - shape = RoundedCornerShape(8.dp) - ) { - Text(text) - } -} - -@Composable -fun HintTextLearnMoreButton( - uri: String = "https://www.das-e-rezept-fuer-deutschland.de/faq" -) { - val uriHandler = LocalUriHandler.current - val offset = ButtonDefaults.TextButtonContentPadding.calculateLeftPadding(LocalLayoutDirection.current) - - TextButton( - modifier = Modifier.absoluteOffset(x = -offset), - onClick = { uriHandler.openUri(uri) }, - enabled = true, - shape = RoundedCornerShape(8.dp) - ) { - Text(App.strings.learnMoreBtn()) - } -} - -@Composable -fun HintCloseButton( - innerPadding: PaddingValues, - onClick: () -> Unit -) { - IconButton( - modifier = Modifier - .padding(top = 2.dp, end = 2.dp), - onClick = onClick - ) { - Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colors.primary) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/OverlayPopup.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/OverlayPopup.kt deleted file mode 100644 index 289d5716..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/OverlayPopup.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.DrawerDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.BlurEffect -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun OverlayPopup( - visible: Boolean, - popupContent: @Composable () -> Unit, - content: @Composable () -> Unit -) { - val transition = updateTransition(visible) - val alpha by transition.animateFloat(transitionSpec = { tween() }) { if (it) 0.5f else 0f } - - val blurModifier = if (visible) { - Modifier.graphicsLayer { - renderEffect = BlurEffect(15f, 15f) - } - } else { - Modifier - } - - Box(Modifier.fillMaxSize()) { - Box(blurModifier) { - content() - } - - val color = DrawerDefaults.scrimColor - - Canvas( - Modifier - .fillMaxSize() - ) { - drawRect(color, alpha = alpha) - } - - Box { - transition.AnimatedVisibility( - visible = { it }, - modifier = Modifier.fillMaxSize(), - exit = fadeOut(animationSpec = tween()) + shrinkVertically( - shrinkTowards = Alignment.Top, - animationSpec = tween() - ), - enter = fadeIn(animationSpec = tween()) + expandVertically( - expandFrom = Alignment.Top, - animationSpec = tween() - ) - ) { - Box { - val shape = RoundedCornerShape(16.dp) - Box( - modifier = Modifier - .padding(32.dp) - .widthIn(max = 720.dp) - .height(560.dp) - .fillMaxHeight(0.7f) - .align(Alignment.Center) - .shadow(6.dp, shape, false) - .clip(shape) - ) { - popupContent() - } - } - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Splittable.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Splittable.kt deleted file mode 100644 index 8d4dd874..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/Splittable.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common - -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.input.pointer.pointerMoveFilter -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.unit.dp -import java.awt.Cursor -import kotlin.math.max -import kotlin.math.min - -@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) -@Composable -fun HorizontalSplittable( - split: Float = 0.5f, - min: Float = 0.3f, - max: Float = 0.7f, - modifier: Modifier = Modifier, - contentLeft: @Composable () -> Unit, - contentRight: @Composable () -> Unit -) { - require(min < max) - - var weight by remember(split) { mutableStateOf(max(min(split, max), min)) } - var width by remember { mutableStateOf(1) } - var hovered by remember { mutableStateOf(false) } - var dragging by remember { mutableStateOf(false) } - val pointer = remember(hovered, dragging) { - if (hovered || dragging) { - PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)) - } else { - PointerIcon.Default - } - } - - Box(modifier.pointerHoverIcon(pointer)) { - Row( - Modifier.fillMaxSize().onSizeChanged { - width = it.width - } - ) { - Box(Modifier.weight(weight)) { - contentLeft() - } - Box( - Modifier.fillMaxHeight() - .width(2.dp) - .draggable( - orientation = Orientation.Horizontal, - onDragStarted = { - dragging = true - }, - onDragStopped = { - dragging = false - }, - state = rememberDraggableState { delta -> - weight = max(min(weight + delta / width, max), min) - } - ) - .pointerMoveFilter( - onEnter = { - hovered = true - false - }, - onExit = { - hovered = false - false - } - ) - ) { - VerticalDivider() - } - Box(Modifier.weight(1f - weight)) { - contentRight() - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/pinning/Pinning.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/pinning/Pinning.kt deleted file mode 100644 index d038b9c2..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/pinning/Pinning.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -// -// DO NOT MODIFY - GENERATED ON 2021-09-20T18:49:22.365036 -// -@file:Suppress("RedundantVisibilityModifier") - -package de.gematik.ti.erp.app.common.pinning - -import okhttp3.CertificatePinner - -public fun buildCertificatePinner(): CertificatePinner = CertificatePinner.Builder() - .add("erp-ref.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - .add("erp-test.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - .add("erp.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - .add("apovzd.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - .add("erp-ref.app.ti-dienste.de", "sha256/RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=") - // expires on 2030-09-23T00:00 - .add("erp-ref.app.ti-dienste.de", "sha256/e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM=") - // expires on 2030-09-23T00:00 - .add("erp-test.app.ti-dienste.de", "sha256/RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=") - // expires on 2030-09-23T00:00 - .add("erp-test.app.ti-dienste.de", "sha256/e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM=") - // expires on 2030-09-23T00:00 - .add("erp.app.ti-dienste.de", "sha256/RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho=") - // expires on 2030-09-23T00:00 - .add("erp.app.ti-dienste.de", "sha256/e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM=") - // expires on 2026-09-21T00:00 - .add("idp-ref.app.ti-dienste.de", "sha256/86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I=") - // expires on 2026-09-21T00:00 - .add("idp-test.app.ti-dienste.de", "sha256/86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I=") - // expires on 2026-09-21T00:00 - .add("idp.app.ti-dienste.de", "sha256/86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I=") - // expires on 2030-09-23T00:00 - .add("apovzd.app.ti-dienste.de", "sha256/e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM=") - // expires on 2031-04-13 - .add("erp-ref.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - // expires on 2024-01-30" - .add("erp-test.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - // expires on 2024-01-30 - .add("erp.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - // expires on 2024-07-07 - .add("apovzd.app.ti-dienste.de", "sha256/qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E=") - // expires on 2028-11-21 - .add("idp-ref.app.ti-dienste.de", "sha256/OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg=") - // expires on 2028-11-21 - .add("idp-test.app.ti-dienste.de", "sha256/OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg=") - // expires on 2028-11-21 - .add("idp.app.ti-dienste.de", "sha256/OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg=") - // expired on 2030-07-02 - .add("erp-dev.app.ti-dienste.de", "sha256/w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4=") - // expired on 2030-07-02 - .add("erp-ref.app.ti-dienste.de", "sha256/w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4=") - // expired on 2030-07-02 - .add("erp-test.app.ti-dienste.de", "sha256/w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4=") - // expired on 2030-07-02 - .add("erp.app.ti-dienste.de", "sha256/w9I8WrpHr1YAS0DugvLGsrI9Vm1yEkZAyllKxemXXf4=") - .build() diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/StringResource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/StringResource.kt deleted file mode 100644 index 59511e0f..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/StringResource.kt +++ /dev/null @@ -1,5860 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -// -// DO NOT MODIFY - GENERATED ON 2022-01-07T10:42:21.252777 -// -@file:Suppress("RedundantVisibilityModifier", "ktlint") - -package de.gematik.ti.erp.app.common.strings - -import androidx.compose.runtime.Stable -import java.util.Locale - -@Stable -public class Strings( - src: Map -) { - @Stable - public val appName: Translatable = src.getValue(0) - - @Stable - public val ok: Translatable = src.getValue(1) - - @Stable - public val understand: Translatable = src.getValue(2) - - @Stable - public val cancel: Translatable = src.getValue(3) - - @Stable - public val back: Translatable = src.getValue(4) - - @Stable - public val at: Translatable = src.getValue(5) - - @Stable - public val descriptiveDateAppendix: Translatable = src.getValue(6) - - @Stable - public val auditProtocolLastUpdateInfo: Translatable = src.getValue(7) - - @Stable - public val auditProtocolSyncFailed: Translatable = src.getValue(8) - - @Stable - public val onBoardingPage1Header: Translatable = src.getValue(9) - - @Stable - public val onBoardingPage1Headline: Translatable = src.getValue(10) - - @Stable - public val onBoardingPage2Header: Translatable = src.getValue(11) - - @Stable - public val onBoardingPage2Info: Translatable = src.getValue(12) - - @Stable - public val onBoardingPage3Header: Translatable = src.getValue(13) - - @Stable - public val onBoardingPage3InfoCheck1: Translatable = src.getValue(14) - - @Stable - public val onBoardingPage3InfoCheck2: Translatable = src.getValue(15) - - @Stable - public val onBoardingPage3InfoCheck3: Translatable = src.getValue(16) - - @Stable - public val onBoardingPage4Header: Translatable = src.getValue(17) - - @Stable - public val onBoardingPage4Info: Translatable = src.getValue(18) - - @Stable - public val onBoardingPage4InfoAcceptInfo: Translatable = src.getValue(19) - - @Stable - public val onBoardingPage4InfoTos: Translatable = src.getValue(20) - - @Stable - public val onBoardingPage4InfoDataprotection: Translatable = src.getValue(21) - - @Stable - public val onBoardingPage4Next: Translatable = src.getValue(22) - - @Stable - public val onBoardingCdnBtnNext: Translatable = src.getValue(23) - - @Stable - public val onBoardingCdnBtnAccept: Translatable = src.getValue(24) - - @Stable - public val camEduHeadline: Translatable = src.getValue(25) - - @Stable - public val camEduDescription: Translatable = src.getValue(26) - - @Stable - public val camEduAccept: Translatable = src.getValue(27) - - @Stable - public val taskId: Translatable = src.getValue(28) - - @Stable - public val accessCode: Translatable = src.getValue(29) - - @Stable - public val copiedClipboard: Translatable = src.getValue(30) - - @Stable - public val consistentPassword: Translatable = src.getValue(31) - - @Stable - public val onbTermsOfUse: Translatable = src.getValue(32) - - @Stable - public val onbDataConsent: Translatable = src.getValue(33) - - @Stable - public val onbAcceptTos: Translatable = src.getValue(34) - - @Stable - public val onbAcceptData: Translatable = src.getValue(35) - - @Stable - public val mainNavTitlePrescriptions: Translatable = src.getValue(36) - - @Stable - public val presBottombarPrescriptions: Translatable = src.getValue(37) - - @Stable - public val presBottombarMessages: Translatable = src.getValue(38) - - @Stable - public val presBottombarPharmacies: Translatable = src.getValue(39) - - @Stable - public val prescriptionGroupSubmitReady: Translatable = src.getValue(40) - - @Stable - public val prescriptionItemExpirationDaysNew: Translatable = src.getValue(41) - - @Stable - public val prescriptionItemAcceptDays: Translatable = src.getValue(42) - - @Stable - public val presEditTxtPlaceHolder: Translatable = src.getValue(43) - - @Stable - public val txtNumberOfPrescriptionsAvailable: Translatable = src.getValue(44) - - @Stable - public val txtRecipe: Translatable = src.getValue(45) - - @Stable - public val txtPrescription: Translatable = src.getValue(46) - - @Stable - public val txtBtnAddPrescriptions: Translatable = src.getValue(47) - - @Stable - public val camAccessDeniedHeadline: Translatable = src.getValue(48) - - @Stable - public val camAccessDeniedDescription: Translatable = src.getValue(49) - - @Stable - public val camInfoScanning: Translatable = src.getValue(50) - - @Stable - public val camInfoInvalid: Translatable = src.getValue(51) - - @Stable - public val camInfoDuplicated: Translatable = src.getValue(52) - - @Stable - public val camInfoDetected: Translatable = src.getValue(53) - - @Stable - public val camAccCancel: Translatable = src.getValue(54) - - @Stable - public val camAccTorch: Translatable = src.getValue(55) - - @Stable - public val camCancelMsg: Translatable = src.getValue(56) - - @Stable - public val camCancelOk: Translatable = src.getValue(57) - - @Stable - public val camCancelResume: Translatable = src.getValue(58) - - @Stable - public val camAcceptMlkitTitle: Translatable = src.getValue(59) - - @Stable - public val camAcceptMlkitBody: Translatable = src.getValue(60) - - @Stable - public val camAcceptMlkitAccept: Translatable = src.getValue(61) - - @Stable - public val camAcceptMlkitDecline: Translatable = src.getValue(62) - - @Stable - public val cdwAddCard: Translatable = src.getValue(63) - - @Stable - public val cdwNext: Translatable = src.getValue(64) - - @Stable - public val cdwIntroTitle: Translatable = src.getValue(65) - - @Stable - public val cdwIntroBody: Translatable = src.getValue(66) - - @Stable - public val cdwIntroWhatYouNeed: Translatable = src.getValue(67) - - @Stable - public val cdwIntroWhatYouNeedNoEgk: Translatable = src.getValue(68) - - @Stable - public val cdwIntroWhatYouNeedEgk: Translatable = src.getValue(69) - - @Stable - public val cdwIntroWhatYouNeedPin: Translatable = src.getValue(70) - - @Stable - public val cdwCanCaption: Translatable = src.getValue(71) - - @Stable - public val cdwCanHeadline: Translatable = src.getValue(72) - - @Stable - public val cdwCanDemoInfo: Translatable = src.getValue(73) - - @Stable - public val cdwPinCaption: Translatable = src.getValue(74) - - @Stable - public val cdwPinTitle: Translatable = src.getValue(75) - - @Stable - public val cdwPinDemoInfo: Translatable = src.getValue(76) - - @Stable - public val cdwAuthRetry: Translatable = src.getValue(77) - - @Stable - public val cdwNfcIntroHeadline: Translatable = src.getValue(78) - - @Stable - public val cdwNfcIntroBody: Translatable = src.getValue(79) - - @Stable - public val cdwNfcIntroStep1HeaderOnError: Translatable = src.getValue(80) - - @Stable - public val cdwNfcIntroStep1InfoOnError: Translatable = src.getValue(81) - - @Stable - public val cdwNfcErrorTitleInvalidCertificate: Translatable = src.getValue(82) - - @Stable - public val cdwNfcErrorBodyInvalidCertificate: Translatable = src.getValue(83) - - @Stable - public val cdwNfcIntroStep2HeaderOnPinError: Translatable = src.getValue(84) - - @Stable - public val cdwNfcIntroStep2InfoOnPinError: Translatable = src.getValue(85) - - @Stable - public val cdwNfcIntroStep2HeaderOnCanError: Translatable = src.getValue(86) - - @Stable - public val cdwNfcIntroStep2InfoOnCanError: Translatable = src.getValue(87) - - @Stable - public val cdwNfcIntroStep2HeaderOnCardBlocked: Translatable = src.getValue(88) - - @Stable - public val cdwNfcIntroStep2InfoOnCardBlocked: Translatable = src.getValue(89) - - @Stable - public val cdwNfcDlgCancel: Translatable = src.getValue(90) - - @Stable - public val cdwNfcSearch1Headline: Translatable = src.getValue(91) - - @Stable - public val cdwNfcSearch1Info: Translatable = src.getValue(92) - - @Stable - public val cdwNfcSearch2Headline: Translatable = src.getValue(93) - - @Stable - public val cdwNfcSearch2Info: Translatable = src.getValue(94) - - @Stable - public val cdwNfcSearch3Headline: Translatable = src.getValue(95) - - @Stable - public val cdwNfcSearch3Info: Translatable = src.getValue(96) - - @Stable - public val cdwNfcFoundHeadline: Translatable = src.getValue(97) - - @Stable - public val cdwNfcFoundInfo: Translatable = src.getValue(98) - - @Stable - public val cdwNfcCommunicationHeadlineTrustedChannelEstablished: Translatable = src.getValue(99) - - @Stable - public val cdwNfcCommunicationHeadlineCertificateLoaded: Translatable = src.getValue(100) - - @Stable - public val cdwNfcCommunicationHeadlinePinVerified: Translatable = src.getValue(101) - - @Stable - public val cdwNfcCommunicationHeadlineChallengeSigned: Translatable = src.getValue(102) - - @Stable - public val cdwNfcCommunicationInfo: Translatable = src.getValue(103) - - @Stable - public val cdwNfcTagLostHeadline: Translatable = src.getValue(104) - - @Stable - public val cdwNfcTagLostInfo: Translatable = src.getValue(105) - - @Stable - public val cdwHappyHeadline: Translatable = src.getValue(106) - - @Stable - public val cdwHappyBody: Translatable = src.getValue(107) - - @Stable - public val cdwDemoUseRealHealthcardTitle: Translatable = src.getValue(108) - - @Stable - public val cdwDemoUseRealHealthcard: Translatable = src.getValue(109) - - @Stable - public val cdwDemoWithHealthcard: Translatable = src.getValue(110) - - @Stable - public val cdwDemoWithoutHealthcard: Translatable = src.getValue(111) - - @Stable - public val demoModeActive: Translatable = src.getValue(112) - - @Stable - public val aboutVersion: Translatable = src.getValue(113) - - @Stable - public val aboutBuildhash: Translatable = src.getValue(114) - - @Stable - public val debugMenu: Translatable = src.getValue(115) - - @Stable - public val redeemTitle: Translatable = src.getValue(116) - - @Stable - public val redeemSubtitle: Translatable = src.getValue(117) - - @Stable - public val redeemTxtCodeDescription: Translatable = src.getValue(118) - - @Stable - public val dialogRedeemHeadline: Translatable = src.getValue(119) - - @Stable - public val dialogRedeemInfo: Translatable = src.getValue(120) - - @Stable - public val dialogOrderHeadline: Translatable = src.getValue(121) - - @Stable - public val dialogOrderInfo: Translatable = src.getValue(122) - - @Stable - public val dialogOrderNotPossible: Translatable = src.getValue(123) - - @Stable - public val searchPharmacyTitle: Translatable = src.getValue(124) - - @Stable - public val searchExampleInput: Translatable = src.getValue(125) - - @Stable - public val searchEnableLocationHintHeader: Translatable = src.getValue(126) - - @Stable - public val searchEnableLocationHintInfo: Translatable = src.getValue(127) - - @Stable - public val searchEnableLocationHintEnable: Translatable = src.getValue(128) - - @Stable - public val searchPharmacyOpenUntil: Translatable = src.getValue(129) - - @Stable - public val searchPharmacyContinuousOpen: Translatable = src.getValue(130) - - @Stable - public val legalNoticeMenu: Translatable = src.getValue(131) - - @Stable - public val legalNoticeIssuer: Translatable = src.getValue(132) - - @Stable - public val legalNoticeAddress: Translatable = src.getValue(133) - - @Stable - public val legalNoticeInfo: Translatable = src.getValue(134) - - @Stable - public val legalNoticeResponsibleHeader: Translatable = src.getValue(135) - - @Stable - public val legalNoticeResponsibleName: Translatable = src.getValue(136) - - @Stable - public val legalNoticeContactHeader: Translatable = src.getValue(137) - - @Stable - public val menuLegalNoticeUrl: Translatable = src.getValue(138) - - @Stable - public val legalNoticeEmail: Translatable = src.getValue(139) - - @Stable - public val legalNoticeHintHeader: Translatable = src.getValue(140) - - @Stable - public val legalNoticeHint: Translatable = src.getValue(141) - - @Stable - public val prsLowDetailDefaultName: Translatable = src.getValue(142) - - @Stable - public val prsLowDetailMedicationName: Translatable = src.getValue(143) - - @Stable - public val prsDividerRefreshInfo: Translatable = src.getValue(144) - - @Stable - public val prsDividerRefreshAction: Translatable = src.getValue(145) - - @Stable - public val prsDividerRedeemedInfo: Translatable = src.getValue(146) - - @Stable - public val prsNotRedeemedNote: Translatable = src.getValue(147) - - @Stable - public val prsNrRedeemed: Translatable = src.getValue(148) - - @Stable - public val prsRedeemedOn: Translatable = src.getValue(149) - - @Stable - public val prsAccExpandRedeemed: Translatable = src.getValue(150) - - @Stable - public val legalNoticeLogoText: Translatable = src.getValue(151) - - @Stable - public val legalNoticeEmailText: Translatable = src.getValue(152) - - @Stable - public val menuLegalNoticeUrlInfo: Translatable = src.getValue(153) - - @Stable - public val authHeadline: Translatable = src.getValue(154) - - @Stable - public val authSubtitle: Translatable = src.getValue(155) - - @Stable - public val authInfo: Translatable = src.getValue(156) - - @Stable - public val authSubtitleError: Translatable = src.getValue(157) - - @Stable - public val authInfoError: Translatable = src.getValue(158) - - @Stable - public val authButton: Translatable = src.getValue(159) - - @Stable - public val authMoreHotline: Translatable = src.getValue(160) - - @Stable - public val authMoreWeb: Translatable = src.getValue(161) - - @Stable - public val authPromptHeadline: Translatable = src.getValue(162) - - @Stable - public val authPromptCancel: Translatable = src.getValue(163) - - @Stable - public val authLinkToGematik: Translatable = src.getValue(164) - - @Stable - public val authLinkToGematikText: Translatable = src.getValue(165) - - @Stable - public val authPromptEnterPassword: Translatable = src.getValue(166) - - @Stable - public val authPromptCheckPassword: Translatable = src.getValue(167) - - @Stable - public val authErrorFailedAuthsHeadline: Translatable = src.getValue(168) - - @Stable - public val authErrorFailedAuthsInfo: Translatable = src.getValue(169) - - @Stable - public val settingsHeadline: Translatable = src.getValue(170) - - @Stable - public val settingsUserUnknown: Translatable = src.getValue(171) - - @Stable - public val settingsEhealthcardHeadline: Translatable = src.getValue(172) - - @Stable - public val settingsEhealthcardAddEhealthcard: Translatable = src.getValue(173) - - @Stable - public val settingsDemoHeadline: Translatable = src.getValue(174) - - @Stable - public val settingsDemoInfo: Translatable = src.getValue(175) - - @Stable - public val settingsDemoToggle: Translatable = src.getValue(176) - - @Stable - public val settingsAccessibilityHeadline: Translatable = src.getValue(177) - - @Stable - public val settingsAccessibilityZoomToggle: Translatable = src.getValue(178) - - @Stable - public val settingsAccessibilityZoomInfo: Translatable = src.getValue(179) - - @Stable - public val settingsAppprotectionHeadline: Translatable = src.getValue(180) - - @Stable - public val settingsAppprotectionInfo: Translatable = src.getValue(181) - - @Stable - public val settingsAppprotectionDeviceSecurityHeader: Translatable = src.getValue(182) - - @Stable - public val settingsAppprotectionDeviceSecurityInfo: Translatable = src.getValue(183) - - @Stable - public val settingsAppprotectionDeviceSecurityDisabledInfo: Translatable = src.getValue(184) - - @Stable - public val settingsLegalHeadline: Translatable = src.getValue(185) - - @Stable - public val settingsLegalImprint: Translatable = src.getValue(186) - - @Stable - public val settingsLegalDataprotection: Translatable = src.getValue(187) - - @Stable - public val settingsLegalTos: Translatable = src.getValue(188) - - @Stable - public val tokenHeadline: Translatable = src.getValue(189) - - @Stable - public val accessTokenTitle: Translatable = src.getValue(190) - - @Stable - public val singleSignOnTokenTitle: Translatable = src.getValue(191) - - @Stable - public val noAccessToken: Translatable = src.getValue(192) - - @Stable - public val noSingleSignOnToken: Translatable = src.getValue(193) - - @Stable - public val copied: Translatable = src.getValue(194) - - @Stable - public val copyContentDescription: Translatable = src.getValue(195) - - @Stable - public val prescriptionOverviewHintWelcomeToDemoHeadline: Translatable = src.getValue(196) - - @Stable - public val prescriptionOverviewHintWelcomeToDemoText: Translatable = src.getValue(197) - - @Stable - public val prescriptionOverviewHintLinkToDemoModeHeadline: Translatable = src.getValue(198) - - @Stable - public val prescriptionOverviewHintLinkToDemoModeText: Translatable = src.getValue(199) - - @Stable - public val prescriptionOverviewHintLinkToDemoModeCallToActionText: Translatable = - src.getValue(200) - - @Stable - public val prescriptionOverviewInfoNoRecipes: Translatable = src.getValue(201) - - @Stable - public val prescriptionOverviewHintDefineSecurityHeadline: Translatable = src.getValue(202) - - @Stable - public val prescriptionOverviewHintDefineSecurityText: Translatable = src.getValue(203) - - @Stable - public val prescriptionOverviewHintDefineSecurityCallToActionText: Translatable = - src.getValue(204) - - @Stable - public val prescriptionDetails: Translatable = src.getValue(205) - - @Stable - public val scannedPrescriptionDetailHintHeader: Translatable = src.getValue(206) - - @Stable - public val scannedPrescriptionDetailHintInfo: Translatable = src.getValue(207) - - @Stable - public val scannedPrescriptionDetailRedeemedHintHeader: Translatable = src.getValue(208) - - @Stable - public val scannedPrescriptionDetailRedeemedHintInfo: Translatable = src.getValue(209) - - @Stable - public val scannedPrescriptionDetailRedeemedHintConnect: Translatable = src.getValue(210) - - @Stable - public val scannedPrescriptionDetailInfoHintHeader: Translatable = src.getValue(211) - - @Stable - public val scannedPrescriptionDetailInfoHintInfo: Translatable = src.getValue(212) - - @Stable - public val scannedPrescriptionPlaceholderName: Translatable = src.getValue(213) - - @Stable - public val scannedPrescriptionDetailsMarkAsRedeemed: Translatable = src.getValue(214) - - @Stable - public val scannedPrescriptionDetailsMarkAsUnredeemed: Translatable = src.getValue(215) - - @Stable - public val scannedPrescriptionDelete: Translatable = src.getValue(216) - - @Stable - public val scannedPrescriptionDetailProtocolHeader: Translatable = src.getValue(217) - - @Stable - public val scannedPrescriptionDetailProtocolScannedLabel: Translatable = src.getValue(218) - - @Stable - public val scannedPrescriptionDetailProtocolScannedAt: Translatable = src.getValue(219) - - @Stable - public val presDetailMedicationValidUntil: Translatable = src.getValue(220) - - @Stable - public val presDetailMedicationExpiryUntil: Translatable = src.getValue(221) - - @Stable - public val presDetailMedicationHeader: Translatable = src.getValue(222) - - @Stable - public val presDetailMedicationLabelDosageForm: Translatable = src.getValue(223) - - @Stable - public val presDetailMedicationLabelNormsize: Translatable = src.getValue(224) - - @Stable - public val presDetailMedicationLabelId: Translatable = src.getValue(225) - - @Stable - public val presDetailDosageHeader: Translatable = src.getValue(226) - - @Stable - public val presDetailDosageDefaultInfo: Translatable = src.getValue(227) - - @Stable - public val presDetailPatientHeader: Translatable = src.getValue(228) - - @Stable - public val presDetailPatientLabelName: Translatable = src.getValue(229) - - @Stable - public val presDetailPatientLabelAddress: Translatable = src.getValue(230) - - @Stable - public val presDetailPatientLabelBirthdate: Translatable = src.getValue(231) - - @Stable - public val presDetailPatientLabelInsurance: Translatable = src.getValue(232) - - @Stable - public val presDetailPatientLabelMemberStatus: Translatable = src.getValue(233) - - @Stable - public val presDetailPatientLabelInsuranceId: Translatable = src.getValue(234) - - @Stable - public val presDetailPractitionerHeader: Translatable = src.getValue(235) - - @Stable - public val presDetailPractitionerLabelName: Translatable = src.getValue(236) - - @Stable - public val presDetailPractitionerLabelQualification: Translatable = src.getValue(237) - - @Stable - public val presDetailPractitionerLabelId: Translatable = src.getValue(238) - - @Stable - public val presDetailOrganizationHeader: Translatable = src.getValue(239) - - @Stable - public val presDetailOrganizationLabelName: Translatable = src.getValue(240) - - @Stable - public val presDetailOrganizationLabelAddress: Translatable = src.getValue(241) - - @Stable - public val presDetailOrganizationLabelId: Translatable = src.getValue(242) - - @Stable - public val presDetailOrganizationLabelTelephone: Translatable = src.getValue(243) - - @Stable - public val presDetailOrganizationLabelEmail: Translatable = src.getValue(244) - - @Stable - public val presDetailAccidentHeader: Translatable = src.getValue(245) - - @Stable - public val presDetailAccidentLabelDate: Translatable = src.getValue(246) - - @Stable - public val presDetailAccidentLabelLocation: Translatable = src.getValue(247) - - @Stable - public val presDetailDeleteMsg: Translatable = src.getValue(248) - - @Stable - public val presDetailDeleteYes: Translatable = src.getValue(249) - - @Stable - public val presDetailDeleteNo: Translatable = src.getValue(250) - - @Stable - public val presDetailUnRedeemMsg: Translatable = src.getValue(251) - - @Stable - public val presDetailUnRedeemAll: Translatable = src.getValue(252) - - @Stable - public val presDetailUnRedeemSelected: Translatable = src.getValue(253) - - @Stable - public val presDetailNoctuHeader: Translatable = src.getValue(254) - - @Stable - public val presDetailNoctuInfo: Translatable = src.getValue(255) - - @Stable - public val presDetailAutIdemHeader: Translatable = src.getValue(256) - - @Stable - public val presDetailAutIdemInfo: Translatable = src.getValue(257) - - @Stable - public val pharmDetailPreorderPrescription: Translatable = src.getValue(258) - - @Stable - public val pharmDetailMessangerDelivered: Translatable = src.getValue(259) - - @Stable - public val pharmDetailMailDelivered: Translatable = src.getValue(260) - - @Stable - public val pharmDetailHintHeader: Translatable = src.getValue(261) - - @Stable - public val pharmDetailHint: Translatable = src.getValue(262) - - @Stable - public val pharmDetailOpeningHours: Translatable = src.getValue(263) - - @Stable - public val pharmDetailWebsite: Translatable = src.getValue(264) - - @Stable - public val reserveHeader: Translatable = src.getValue(265) - - @Stable - public val pharmReserveSubheader: Translatable = src.getValue(266) - - @Stable - public val pharmReservePrescriptions: Translatable = src.getValue(267) - - @Stable - public val pharmRedeem: Translatable = src.getValue(268) - - @Stable - public val pharmCourierHeader: Translatable = src.getValue(269) - - @Stable - public val pharmReserveDeliveryAddress: Translatable = src.getValue(270) - - @Stable - public val pharmDeliveryCardHelp: Translatable = src.getValue(271) - - @Stable - public val pharmDeliveryCardMessage: Translatable = src.getValue(272) - - @Stable - public val pharmDeliveryCardCall: Translatable = src.getValue(273) - - @Stable - public val pharmMailHeader: Translatable = src.getValue(274) - - @Stable - public val presDetailProtocolHeader: Translatable = src.getValue(275) - - @Stable - public val presDetailProtocolEmptyText: Translatable = src.getValue(276) - - @Stable - public val prescriptionItemAcceptOnlyToday: Translatable = src.getValue(277) - - @Stable - public val prescriptionItemExpirationOnlyToday: Translatable = src.getValue(278) - - @Stable - public val prescriptionItemExpired: Translatable = src.getValue(279) - - @Stable - public val presEditTxtTitle: Translatable = src.getValue(280) - - @Stable - public val presEditTxtInfo: Translatable = src.getValue(281) - - @Stable - public val cdwTopBarTitle: Translatable = src.getValue(282) - - @Stable - public val cdwAuthBtnTxt: Translatable = src.getValue(283) - - @Stable - public val cdwIntroWhatYouNeedNfc: Translatable = src.getValue(284) - - @Stable - public val cdwEnableNfcHeader: Translatable = src.getValue(285) - - @Stable - public val cdwEnableNfcInfo: Translatable = src.getValue(286) - - @Stable - public val cdwEnableNfcBtnText: Translatable = src.getValue(287) - - @Stable - public val cdwCanInfoHintHeader: Translatable = src.getValue(288) - - @Stable - public val cdwCanInfoHintInfo: Translatable = src.getValue(289) - - @Stable - public val cdwPinInfoHintHeader: Translatable = src.getValue(290) - - @Stable - public val cdwPinInfoHintInfo: Translatable = src.getValue(291) - - @Stable - public val cdwSaveAccessTitle: Translatable = src.getValue(292) - - @Stable - public val cdwSaveWithBiometryTitle: Translatable = src.getValue(293) - - @Stable - public val cdwSaveWithBiometryInfo: Translatable = src.getValue(294) - - @Stable - public val cdwBiometricNotPossibleTitle: Translatable = src.getValue(295) - - @Stable - public val cdwBiometricNotPossibleInfo: Translatable = src.getValue(296) - - @Stable - public val cdwNotSaveWithBiometryTitle: Translatable = src.getValue(297) - - @Stable - public val cdwNotSaveWithBiometryInfo: Translatable = src.getValue(298) - - @Stable - public val cdwAuthRetryPinCan: Translatable = src.getValue(299) - - @Stable - public val cdwHappyBackToMainscreen: Translatable = src.getValue(300) - - @Stable - public val redeemProtocolText: Translatable = src.getValue(301) - - @Stable - public val unRedeemProtocolText: Translatable = src.getValue(302) - - @Stable - public val redeemShowSingleCodes: Translatable = src.getValue(303) - - @Stable - public val redeemShowCollectiveCodes: Translatable = src.getValue(304) - - @Stable - public val redeemCounterText: Translatable = src.getValue(305) - - @Stable - public val redeemPrescriptionsDialogHeader: Translatable = src.getValue(306) - - @Stable - public val redeemPrescriptionsDialogInfo: Translatable = src.getValue(307) - - @Stable - public val redeemPrescriptionsDialogCancel: Translatable = src.getValue(308) - - @Stable - public val redeemPrescriptionsDialogRedeem: Translatable = src.getValue(309) - - @Stable - public val searchPharmacyOpensAt: Translatable = src.getValue(310) - - @Stable - public val technicalHotlineContact: Translatable = src.getValue(311) - - @Stable - public val technicalHotlineInfo: Translatable = src.getValue(312) - - @Stable - public val mainScanAcc: Translatable = src.getValue(313) - - @Stable - public val mainSettingsAcc: Translatable = src.getValue(314) - - @Stable - public val authHotlinephoneContact: Translatable = src.getValue(315) - - @Stable - public val authPromptInfoBiometrics: Translatable = src.getValue(316) - - @Stable - public val settingsScreenshotsAlertHeadline: Translatable = src.getValue(317) - - @Stable - public val settingsScreenshotsAlertInfo: Translatable = src.getValue(318) - - @Stable - public val settingsScreenshotsButtonText: Translatable = src.getValue(319) - - @Stable - public val settingsTrackingHeadline: Translatable = src.getValue(320) - - @Stable - public val settingsTrackingInfo: Translatable = src.getValue(321) - - @Stable - public val settingsTrackingToggleText: Translatable = src.getValue(322) - - @Stable - public val settingsTrackingDescription: Translatable = src.getValue(323) - - @Stable - public val settingsScreenshotsToggleText: Translatable = src.getValue(324) - - @Stable - public val settingsScreenshotsDescription: Translatable = src.getValue(325) - - @Stable - public val settingsTrackingDialogTitle: Translatable = src.getValue(326) - - @Stable - public val settingsTrackingDialogText: Translatable = src.getValue(327) - - @Stable - public val settingsTrackingAllow: Translatable = src.getValue(328) - - @Stable - public val settingsDeviceSecurityAllow: Translatable = src.getValue(329) - - @Stable - public val settingsAppprotectionModePasswordHeadline: Translatable = src.getValue(330) - - @Stable - public val settingsAppprotectionModePasswordInfo: Translatable = src.getValue(331) - - @Stable - public val settingsPasswordHeadline: Translatable = src.getValue(332) - - @Stable - public val settingsPasswordSave: Translatable = src.getValue(333) - - @Stable - public val settingsPasswordAccShowPasswordToggle: Translatable = src.getValue(334) - - @Stable - public val settingsPasswordEnterPassword: Translatable = src.getValue(335) - - @Stable - public val settingsPasswordHint: Translatable = src.getValue(336) - - @Stable - public val settingsPasswordRepeatPassword: Translatable = src.getValue(337) - - @Stable - public val settingsPasswordStrength: Translatable = src.getValue(338) - - @Stable - public val settingsPasswordSuggestions: Translatable = src.getValue(339) - - @Stable - public val prescriptionOverviewHintNewPrescriptionsHeadline: Translatable = src.getValue(340) - - @Stable - public val prescriptionOverviewHintNewPrescriptionsText: Translatable = src.getValue(341) - - @Stable - public val prescriptionOverviewHintNewPrescriptionsCallToActionText: Translatable = - src.getValue(342) - - @Stable - public val presDetailProtocolShowAll: Translatable = src.getValue(343) - - @Stable - public val presDetailDelete: Translatable = src.getValue(344) - - @Stable - public val presDetailRedeemInfo: Translatable = src.getValue(345) - - @Stable - public val presDetailUnRedeem: Translatable = src.getValue(346) - - @Stable - public val presDetailShowMore: Translatable = src.getValue(347) - - @Stable - public val presDetailShowLess: Translatable = src.getValue(348) - - @Stable - public val presDetailTechnicalInformation: Translatable = src.getValue(349) - - @Stable - public val logout: Translatable = src.getValue(350) - - @Stable - public val logoutDescription: Translatable = src.getValue(351) - - @Stable - public val logoutDetailMessage: Translatable = src.getValue(352) - - @Stable - public val logoutDeleteYes: Translatable = src.getValue(353) - - @Stable - public val logoutDeleteNo: Translatable = src.getValue(354) - - @Stable - public val logoutDetailHeader: Translatable = src.getValue(355) - - @Stable - public val settingsBiometricDialogTitle: Translatable = src.getValue(356) - - @Stable - public val settingsBiometricDialogText: Translatable = src.getValue(357) - - @Stable - public val redeemOnlineDetailHeader: Translatable = src.getValue(358) - - @Stable - public val redeemOnlineDetailMessage: Translatable = src.getValue(359) - - @Stable - public val redeemOnlineNo: Translatable = src.getValue(360) - - @Stable - public val redeemOnlineYes: Translatable = src.getValue(361) - - @Stable - public val redeemOnlineSuccessHeader: Translatable = src.getValue(362) - - @Stable - public val redeemOnlineCourierSuccessMessage: Translatable = src.getValue(363) - - @Stable - public val redeemOnlineMailSuccessHeader: Translatable = src.getValue(364) - - @Stable - public val redeemOnlineMailSuccessStep1: Translatable = src.getValue(365) - - @Stable - public val redeemOnlineMailSuccessStep2: Translatable = src.getValue(366) - - @Stable - public val redeemOnlineMailSuccessStep3: Translatable = src.getValue(367) - - @Stable - public val redeemOnlineBackHome: Translatable = src.getValue(368) - - @Stable - public val redeemOnlineErrorUploading: Translatable = src.getValue(369) - - @Stable - public val redeemOnlineErrorRetryLabel: Translatable = src.getValue(370) - - @Stable - public val redeemOnlineLocalSuccessMessage: Translatable = src.getValue(371) - - @Stable - public val communicationShipmentInboxHeader: Translatable = src.getValue(372) - - @Stable - public val communicationLocalInboxHeader: Translatable = src.getValue(373) - - @Stable - public val communicationDeliveryInboxHeader: Translatable = src.getValue(374) - - @Stable - public val communicationLocalActionText: Translatable = src.getValue(375) - - @Stable - public val communicationShipmentActionText: Translatable = src.getValue(376) - - @Stable - public val pickupScreenInfo: Translatable = src.getValue(377) - - @Stable - public val pickupScreenTitle: Translatable = src.getValue(378) - - @Stable - public val messagesEmptyScreen: Translatable = src.getValue(379) - - @Stable - public val messagesEmptyScreenInfo: Translatable = src.getValue(380) - - @Stable - public val communicationInfoTextNotAvailable: Translatable = src.getValue(381) - - @Stable - public val contactEmailNoClient: Translatable = src.getValue(382) - - @Stable - public val searchPharmacyNothingFoundHeader: Translatable = src.getValue(383) - - @Stable - public val searchPharmacyNothingFoundInfo: Translatable = src.getValue(384) - - @Stable - public val searchPharmacyErrorTitle: Translatable = src.getValue(385) - - @Stable - public val searchPharmacyErrorSubtitle: Translatable = src.getValue(386) - - @Stable - public val searchPharmacyErrorAction: Translatable = src.getValue(387) - - @Stable - public val settingsLegalLicences: Translatable = src.getValue(388) - - @Stable - public val settingsContactHeadline: Translatable = src.getValue(389) - - @Stable - public val settingsContactHotline: Translatable = src.getValue(390) - - @Stable - public val settingsContactMail: Translatable = src.getValue(391) - - @Stable - public val settingsContactFeedback: Translatable = src.getValue(392) - - @Stable - public val settingsContactFeedbackForm: Translatable = src.getValue(393) - - @Stable - public val settingsContactHotlineNumber: Translatable = src.getValue(394) - - @Stable - public val settingsContactMailAddress: Translatable = src.getValue(395) - - @Stable - public val settingsContactFeedbackAdress: Translatable = src.getValue(396) - - @Stable - public val settingsShowToken: Translatable = src.getValue(397) - - @Stable - public val learnMoreBtn: Translatable = src.getValue(398) - - @Stable - public val onBoardingPage1AccImage: Translatable = src.getValue(399) - - @Stable - public val onBoardingPage2AccImage: Translatable = src.getValue(400) - - @Stable - public val onBoardingPage3AccImage: Translatable = src.getValue(401) - - @Stable - public val onBoardingPage5Header: Translatable = src.getValue(402) - - @Stable - public val onBoardingPage5SubHeader: Translatable = src.getValue(403) - - @Stable - public val onBoardingPage5Info1: Translatable = src.getValue(404) - - @Stable - public val onBoardingPage5Info2: Translatable = src.getValue(405) - - @Stable - public val onBoardingPage5Info3: Translatable = src.getValue(406) - - @Stable - public val onBoardingPage5Label: Translatable = src.getValue(407) - - @Stable - public val onBoardingPage5LabelInfo: Translatable = src.getValue(408) - - @Stable - public val onBoardingPage5Anonym: Translatable = src.getValue(409) - - @Stable - public val onBoardingPage5Next: Translatable = src.getValue(410) - - @Stable - public val onBoardingSecureAppPageHeader: Translatable = src.getValue(411) - - @Stable - public val onBoardingSecureAppPageInfo: Translatable = src.getValue(412) - - @Stable - public val onboardingSecureAppOr: Translatable = src.getValue(413) - - @Stable - public val onboardingSecureAppButtonBest: Translatable = src.getValue(414) - - @Stable - public val onboardingSecureAppButtonBestChosen: Translatable = src.getValue(415) - - @Stable - public val onboardingSecureAppButtonBestInfo: Translatable = src.getValue(416) - - @Stable - public val settingsTrackingDialogText1: Translatable = src.getValue(417) - - @Stable - public val settingsTrackingDialogText2: Translatable = src.getValue(418) - - @Stable - public val settingsTrackingDialogText3: Translatable = src.getValue(419) - - @Stable - public val settingsTrackingAllowTitle: Translatable = src.getValue(420) - - @Stable - public val settingsTrackingNotAllow: Translatable = src.getValue(421) - - @Stable - public val settingsTrackingDisallowInfo: Translatable = src.getValue(422) - - @Stable - public val settingsTrackingAllowInfo: Translatable = src.getValue(423) - - @Stable - public val settingsTrackingAllowEmoji: Translatable = src.getValue(424) - - @Stable - public val settingsFeedbackFormHeader: Translatable = src.getValue(425) - - @Stable - public val settingsFeedbackFormPlaceholder: Translatable = src.getValue(426) - - @Stable - public val seetingsFeedbackFormAdditionalDataInfo: Translatable = src.getValue(427) - - @Stable - public val seetingsFeedbackFormAdditionalDataOs: Translatable = src.getValue(428) - - @Stable - public val seetingsFeedbackFormAdditionalDataOsDetail: Translatable = src.getValue(429) - - @Stable - public val seetingsFeedbackFormAdditionalDataDevice: Translatable = src.getValue(430) - - @Stable - public val seetingsFeedbackFormAdditionalDataDeviceDetail: Translatable = src.getValue(431) - - @Stable - public val seetingsFeedbackFormAdditionalDataDarkmode: Translatable = src.getValue(432) - - @Stable - public val seetingsFeedbackFormAdditionalDataDarkmodeOn: Translatable = src.getValue(433) - - @Stable - public val seetingsFeedbackFormAdditionalDataDarkmodeOff: Translatable = src.getValue(434) - - @Stable - public val seetingsFeedbackFormAdditionalDataLanguage: Translatable = src.getValue(435) - - @Stable - public val settingsFeedbackFormSend: Translatable = src.getValue(436) - - @Stable - public val settingsFeedbackFormHeadline: Translatable = src.getValue(437) - - @Stable - public val presDetailMedicationRedeemButtonText: Translatable = src.getValue(438) - - @Stable - public val redeemSyncedPrescriptionsDialogHeader: Translatable = src.getValue(439) - - @Stable - public val redeemSyncedPrescriptionsDialogInfo: Translatable = src.getValue(440) - - @Stable - public val redeemSyncedPrescriptionsDialogOk: Translatable = src.getValue(441) - - @Stable - public val logoutDeleteNoAccess: Translatable = src.getValue(442) - - @Stable - public val communicationErrorActionText: Translatable = src.getValue(443) - - @Stable - public val communicationErrorInboxHeader: Translatable = src.getValue(444) - - @Stable - public val communicationErrorInboxDisplayText: Translatable = src.getValue(445) - - @Stable - public val messagesContactMailAddress: Translatable = src.getValue(446) - - @Stable - public val messagesContactEmailSubject: Translatable = src.getValue(447) - - @Stable - public val messagesContactEmailBody: Translatable = src.getValue(448) - - @Stable - public val messagesContactEmailDataTransparency: Translatable = src.getValue(449) - - @Stable - public val messagesContactEmailErrorCode: Translatable = src.getValue(450) - - @Stable - public val insecureDeviceTitle: Translatable = src.getValue(451) - - @Stable - public val insecureDeviceHeader: Translatable = src.getValue(452) - - @Stable - public val insecureDeviceInfo: Translatable = src.getValue(453) - - @Stable - public val insecureDeviceAccept: Translatable = src.getValue(454) - - @Stable - public val alternateAuthHeader: Translatable = src.getValue(455) - - @Stable - public val alternateAuthInfo: Translatable = src.getValue(456) - - @Stable - public val presDetailMedicationRedeemedOn: Translatable = src.getValue(457) - - @Stable - public val presDetailSubstitutedHeader: Translatable = src.getValue(458) - - @Stable - public val presDetailSubstitutedInfo: Translatable = src.getValue(459) - - @Stable - public val pharmacyDetailDataInfo: Translatable = src.getValue(460) - - @Stable - public val pharmacyDetailDataInfoDomain: Translatable = src.getValue(461) - - @Stable - public val pharmacyDetailDataInfoBtn: Translatable = src.getValue(462) - - @Stable - public val pharmacyDetailPharmacyPortalUri: Translatable = src.getValue(463) - - @Stable - public val pharmacyDetailDataInfoFaqsUri: Translatable = src.getValue(464) - - @Stable - public val pharmacyDetailNotReadyHeader: Translatable = src.getValue(465) - - @Stable - public val pharmacyDetailNotReadyInfo: Translatable = src.getValue(466) - - @Stable - public val searchPharmacyReadyFlag: Translatable = src.getValue(467) - - @Stable - public val searchPharmaciesFilterReady: Translatable = src.getValue(468) - - @Stable - public val searchPharmaciesFilterOpenNow: Translatable = src.getValue(469) - - @Stable - public val searchPharmaciesFilterDeliveryService: Translatable = src.getValue(470) - - @Stable - public val searchPharmaciesFilterOnlineService: Translatable = src.getValue(471) - - @Stable - public val searchPharmaciesFilterHeader: Translatable = src.getValue(472) - - @Stable - public val searchPharmaciesFilterSectionFavorites: Translatable = src.getValue(473) - - @Stable - public val searchPharmaciesFilter: Translatable = src.getValue(474) - - @Stable - public val searchPharmaciesLocationNaInfo: Translatable = src.getValue(475) - - @Stable - public val searchPharmaciesLocationNaHeader: Translatable = src.getValue(476) - - @Stable - public val errorMessageNetworkNotAvailable: Translatable = src.getValue(477) - - @Stable - public val errorMessageServerCommunicationFailed: Translatable = src.getValue(478) - - @Stable - public val errorMessageVauError: Translatable = src.getValue(479) - - @Stable - public val settingsNoActiveToken: Translatable = src.getValue(480) - - @Stable - public val prescriptionDetailRedeemed: Translatable = src.getValue(481) - - @Stable - public val prescriptionDetailUnRedeemed: Translatable = src.getValue(482) - - @Stable - public val insecureDeviceTitleSafetynet: Translatable = src.getValue(483) - - @Stable - public val insecureDeviceHeaderSafetynet: Translatable = src.getValue(484) - - @Stable - public val insecureDeviceInfoSafetynet: Translatable = src.getValue(485) - - @Stable - public val insecureDeviceAcceptSafetynet: Translatable = src.getValue(486) - - @Stable - public val insecureDeviceSafetynetMoreInfo: Translatable = src.getValue(487) - - @Stable - public val insecureDeviceSafetynetLinkText: Translatable = src.getValue(488) - - @Stable - public val insecureDeviceSafetynetLink: Translatable = src.getValue(489) - - @Stable - public val cdwHealthCardInfoTitle: Translatable = src.getValue(490) - - @Stable - public val cdwHealthcardInfoHeadline: Translatable = src.getValue(491) - - @Stable - public val cdwHealthcardInfo: Translatable = src.getValue(492) - - @Stable - public val cdwHealthcardInfoBullet: Translatable = src.getValue(493) - - @Stable - public val cdwHealthcardInfoCan: Translatable = src.getValue(494) - - @Stable - public val cdwHealthcardInfoCard: Translatable = src.getValue(495) - - @Stable - public val cdwHealthcardInfoPin: Translatable = src.getValue(496) - - @Stable - public val cdwHealthcardInfoPinPin: Translatable = src.getValue(497) - - @Stable - public val cdwHealthCardInfoHintDescription: Translatable = src.getValue(498) - - @Stable - public val cdwHealthcardInfoMailDescription: Translatable = src.getValue(499) - - @Stable - public val cdwHealthcardInfoMail: Translatable = src.getValue(500) - - @Stable - public val cdwRegisterWithHealthyCard: Translatable = src.getValue(501) - - @Stable - public val cdwRegisterWithHealthInsurance: Translatable = src.getValue(502) - - @Stable - public val cdwRegisterHealtyCardInfo: Translatable = src.getValue(503) - - @Stable - public val cdwRegisterHealthInsuranceInfo: Translatable = src.getValue(504) - - @Stable - public val cdwRegisterTitle: Translatable = src.getValue(505) - - @Stable - public val cdwRegisterBody: Translatable = src.getValue(506) - - @Stable - public val cdwRegisterScreenTitle: Translatable = src.getValue(507) - - @Stable - public val cdwManRegisterAccessibility: Translatable = src.getValue(508) - - @Stable - public val cdwWomanRegisterAccessibility: Translatable = src.getValue(509) - - @Stable - public val cdwNoNfc: Translatable = src.getValue(510) - - @Stable - public val profileEditName: Translatable = src.getValue(511) - - @Stable - public val profileEditNameInfo: Translatable = src.getValue(512) - - @Stable - public val profileEditNamePlaceHolder: Translatable = src.getValue(513) - - @Stable - public val settingsProfileEditName: Translatable = src.getValue(514) - - @Stable - public val settingsProfileDelete: Translatable = src.getValue(515) - - @Stable - public val settingsProfilesHeadline: Translatable = src.getValue(516) - - @Stable - public val onboardingProfileHeader: Translatable = src.getValue(517) - - @Stable - public val onboardingProfileInfo: Translatable = src.getValue(518) - - @Stable - public val onboardingProfileInputName: Translatable = src.getValue(519) - - @Stable - public val settingsAddProfilesHintTitle: Translatable = src.getValue(520) - - @Stable - public val settingsAddProfilesHintInfo: Translatable = src.getValue(521) - - @Stable - public val settingsAddProfile: Translatable = src.getValue(522) - - @Stable - public val profileSetupSave: Translatable = src.getValue(523) - - @Stable - public val cdwHealthInsurancePageTitle: Translatable = src.getValue(524) - - @Stable - public val cdwHealthInsuranceTitle: Translatable = src.getValue(525) - - @Stable - public val cdwHealthInsuranceBodyWhatYouNeed: Translatable = src.getValue(526) - - @Stable - public val cdwHealthInsuranceBodyHowToGet: Translatable = src.getValue(527) - - @Stable - public val cdwHealthInsuranceCaptionRecognizeHealthcard: Translatable = src.getValue(528) - - @Stable - public val cdwHealthInsuranceLearnMore: Translatable = src.getValue(529) - - @Stable - public val cdwHealthInsuranceSelectCompany: Translatable = src.getValue(530) - - @Stable - public val cdwHealthInsuranceNoCompanySelected: Translatable = src.getValue(531) - - @Stable - public val cdwHealthInsuranceWhatToDo: Translatable = src.getValue(532) - - @Stable - public val cdwHealthInsuranceNoCantactsTitle: Translatable = src.getValue(533) - - @Stable - public val cdwHealthInsuranceNoCantactsBody: Translatable = src.getValue(534) - - @Stable - public val cdwHealthInsuranceContactHealthcardPin: Translatable = src.getValue(535) - - @Stable - public val cdwHealthInsuranceContactPinOnly: Translatable = src.getValue(536) - - @Stable - public val cdwHealthInsuranceContactInsuranceCompany: Translatable = src.getValue(537) - - @Stable - public val cdwHealthInsuranceMailSubject: Translatable = src.getValue(538) - - @Stable - public val settingsHealthInsuranceContactTitle: Translatable = src.getValue(539) - - @Stable - public val settingsHealthInsuranceContactBody: Translatable = src.getValue(540) - - @Stable - public val settingsHealthInsuranceContactAction: Translatable = src.getValue(541) - - @Stable - public val cdwCapabilityTitle: Translatable = src.getValue(542) - - @Stable - public val cdwCapabilityHeadline: Translatable = src.getValue(543) - - @Stable - public val cdwCapabilityBody: Translatable = src.getValue(544) - - @Stable - public val cdwCapabilityMore: Translatable = src.getValue(545) - - @Stable - public val editProfileEmptyProfileName: Translatable = src.getValue(546) - - @Stable - public val editProfileDuplicatedProfileName: Translatable = src.getValue(547) - - @Stable - public val editProfileTitle: Translatable = src.getValue(548) - - @Stable - public val editProfileColorSelected: Translatable = src.getValue(549) - - @Stable - public val editProfileBackgroundColor: Translatable = src.getValue(550) - - @Stable - public val profileColorNameGray: Translatable = src.getValue(551) - - @Stable - public val profileColorSunDew: Translatable = src.getValue(552) - - @Stable - public val profileColorNamePink: Translatable = src.getValue(553) - - @Stable - public val profileColorNameTree: Translatable = src.getValue(554) - - @Stable - public val profileColorNameMoon: Translatable = src.getValue(555) - - @Stable - public val settingsProfileNotConnected: Translatable = src.getValue(556) - - @Stable - public val settingsProfileConnected: Translatable = src.getValue(557) - - @Stable - public val settingsProfileLastAuthenticatedOn: Translatable = src.getValue(558) - - @Stable - public val removeProfileHeader: Translatable = src.getValue(559) - - @Stable - public val removeProfileDetailMessage: Translatable = src.getValue(560) - - @Stable - public val removeProfileYes: Translatable = src.getValue(561) - - @Stable - public val removeProfileNo: Translatable = src.getValue(562) - - @Stable - public val removeProfile: Translatable = src.getValue(563) - - @Stable - public val profileEditNameForDefault: Translatable = src.getValue(564) - - @Stable - public val profileEditNameForDefaultInfo: Translatable = src.getValue(565) - - @Stable - public val cdwNfcErrorTitleInvalidOcspResponseOfHealthCardCertificate: Translatable = - src.getValue(566) - - @Stable - public val cdwNfcErrorBodyInvalidOcspResponseOfHealthCardCertificate: Translatable = - src.getValue(567) - - @Stable - public val loginDescription: Translatable = src.getValue(568) - - @Stable - public val presDetailHealthPortalDescription: Translatable = src.getValue(569) - - @Stable - public val presDetailHealthPortalDescriptionUrlInfo: Translatable = src.getValue(570) - - @Stable - public val presDetailHealthPortalDescriptionUrl: Translatable = src.getValue(571) - - @Stable - public val selectProfile: Translatable = src.getValue(572) - - @Stable - public val editProfiles: Translatable = src.getValue(573) - - @Stable - public val zeroPrescriptionsUpdatet: Translatable = src.getValue(574) - - @Stable - public val prescriptionsUpdated: Translatable = src.getValue(575) - - @Stable - public val prescriptionStatusReady: Translatable = src.getValue(576) - - @Stable - public val prescriptionStatusInProgress: Translatable = src.getValue(577) - - @Stable - public val prescriptionStatusCompleted: Translatable = src.getValue(578) - - @Stable - public val prescriptionStatusUnknown: Translatable = src.getValue(579) - - @Stable - public val pharmacyDetailTitle: Translatable = src.getValue(580) - - @Stable - public val settingsShowAuditEvents: Translatable = src.getValue(581) - - @Stable - public val settingsShowAuditEventsInfo: Translatable = src.getValue(582) - - @Stable - public val settingsShowTokenInfo: Translatable = src.getValue(583) - - @Stable - public val autiteventsHeadline: Translatable = src.getValue(584) - - @Stable - public val logoutProfile: Translatable = src.getValue(585) - - @Stable - public val loginProfile: Translatable = src.getValue(586) - - @Stable - public val functionNotAvaillableOnDemoMode: Translatable = src.getValue(587) - - @Stable - public val presDetailMedicationInProgress: Translatable = src.getValue(588) - - @Stable - public val directAssignmentWillBeForwardet: Translatable = src.getValue(589) - - @Stable - public val noAuditEventsHeader: Translatable = src.getValue(590) - - @Stable - public val noAuditEventsNotAuthenticatedInfo: Translatable = src.getValue(591) - - @Stable - public val noAuditEventsEmptyProtocolListInfo: Translatable = src.getValue(592) - - @Stable - public val auditEventsUpdatedAt: Translatable = src.getValue(593) - - @Stable - public val logoutDeleteInProgress: Translatable = src.getValue(594) - - @Stable - public val profileNotConnected: Translatable = src.getValue(595) - - @Stable - public val profileConnected: Translatable = src.getValue(596) - - @Stable - public val connectProfileHeader: Translatable = src.getValue(597) - - @Stable - public val connectProfileInfo: Translatable = src.getValue(598) - - @Stable - public val connectProfileConnect: Translatable = src.getValue(599) - - @Stable - public val noTokenAvailableInDemoMode: Translatable = src.getValue(600) - - @Stable - public val settingsAddProfileNotAllowed: Translatable = src.getValue(601) - - @Stable - public val desktopMenuInfo: Translatable = src.getValue(602) - - @Stable - public val desktopMenuTerms: Translatable = src.getValue(603) - - @Stable - public val desktopMenuData: Translatable = src.getValue(604) - - @Stable - public val desktopMainWelcomeTitle: Translatable = src.getValue(605) - - @Stable - public val desktopMainWelcomeSubtitle: Translatable = src.getValue(606) - - @Stable - public val desktopMainLoginWithHealthcard: Translatable = src.getValue(607) - - @Stable - public val desktopMainPrescriptions: Translatable = src.getValue(608) - - @Stable - public val desktopMainCommunications: Translatable = src.getValue(609) - - @Stable - public val desktopMainRedeemablePrescriptions: Translatable = src.getValue(610) - - @Stable - public val desktopMainRedeemedPrescriptions: Translatable = src.getValue(611) - - @Stable - public val desktopMainPharmacyCommunications: Translatable = src.getValue(612) - - @Stable - public val desktopMainPharmacyProtocol: Translatable = src.getValue(613) - - @Stable - public val desktopMainRefresh: Translatable = src.getValue(614) - - @Stable - public val desktopMainZoomIn: Translatable = src.getValue(615) - - @Stable - public val desktopMainZoomOut: Translatable = src.getValue(616) - - @Stable - public val desktopMainHelp: Translatable = src.getValue(617) - - @Stable - public val desktopMainLogout: Translatable = src.getValue(618) - - @Stable - public val desktopPrescriptionNoData: Translatable = src.getValue(619) - - @Stable - public val desktopLoginPageDataTerms: Translatable = src.getValue(620) - - @Stable - public val desktopLoginPageConnectReader: Translatable = src.getValue(621) - - @Stable - public val desktopLoginPageEnterCan: Translatable = src.getValue(622) - - @Stable - public val desktopLoginPageEnterPin: Translatable = src.getValue(623) - - @Stable - public val desktopLoginPageConnectHealthcard: Translatable = src.getValue(624) - - @Stable - public val desktopLoginPageReaderSearchHealthcard: Translatable = src.getValue(625) - - @Stable - public val desktopLoginPageReaderFoundHealthcard: Translatable = src.getValue(626) - - @Stable - public val desktopLoginPageReaderError: Translatable = src.getValue(627) - - @Stable - public val desktopLoginPageConnectHealthcardErrorIoTitle: Translatable = src.getValue(628) - - @Stable - public val desktopLoginPageConnectHealthcardErrorIoSubtitle: Translatable = src.getValue(629) - - @Stable - public val desktopPrescriptionExpiresOn: Translatable = src.getValue(630) - - @Stable - public val desktopPrescriptionAcceptUntil: Translatable = src.getValue(631) - - @Stable - public val desktopPrescriptionExpired: Translatable = src.getValue(632) - - @Stable - public val desktopPrescriptionPrescribedOn: Translatable = src.getValue(633) - - @Stable - public val desktopPrescriptionShowDmc: Translatable = src.getValue(634) - - @Stable - public val desktopCommunicationOnPremise: Translatable = src.getValue(635) - - @Stable - public val desktopCommunicationDelivery: Translatable = src.getValue(636) - - @Stable - public val desktopCommunicationShipment: Translatable = src.getValue(637) - - @Stable - public val desktopTermsLink: Translatable = src.getValue(638) - - @Stable - public val desktopDataLink: Translatable = src.getValue(639) - - @Stable - public val desktopHelpLink: Translatable = src.getValue(640) - - @Stable - public val desktopContactLink: Translatable = src.getValue(641) - - @Stable - public val kbvMemberStatus1: Translatable = src.getValue(642) - - @Stable - public val kbvMemberStatus3: Translatable = src.getValue(643) - - @Stable - public val kbvMemberStatus5: Translatable = src.getValue(644) - - @Stable - public val kbvNormSizeKa: Translatable = src.getValue(645) - - @Stable - public val kbvNormSizeKtp: Translatable = src.getValue(646) - - @Stable - public val kbvNormSizeN1: Translatable = src.getValue(647) - - @Stable - public val kbvNormSizeN2: Translatable = src.getValue(648) - - @Stable - public val kbvNormSizeN3: Translatable = src.getValue(649) - - @Stable - public val kbvNormSizeNb: Translatable = src.getValue(650) - - @Stable - public val kbvNormSizeSonstiges: Translatable = src.getValue(651) - - @Stable - public val kbvCodeDosageFormAeo: Translatable = src.getValue(652) - - @Stable - public val kbvCodeDosageFormAmp: Translatable = src.getValue(653) - - @Stable - public val kbvCodeDosageFormApa: Translatable = src.getValue(654) - - @Stable - public val kbvCodeDosageFormAsn: Translatable = src.getValue(655) - - @Stable - public val kbvCodeDosageFormAso: Translatable = src.getValue(656) - - @Stable - public val kbvCodeDosageFormAto: Translatable = src.getValue(657) - - @Stable - public val kbvCodeDosageFormAtr: Translatable = src.getValue(658) - - @Stable - public val kbvCodeDosageFormAub: Translatable = src.getValue(659) - - @Stable - public val kbvCodeDosageFormAuc: Translatable = src.getValue(660) - - @Stable - public val kbvCodeDosageFormAug: Translatable = src.getValue(661) - - @Stable - public val kbvCodeDosageFormAus: Translatable = src.getValue(662) - - @Stable - public val kbvCodeDosageFormBad: Translatable = src.getValue(663) - - @Stable - public val kbvCodeDosageFormBal: Translatable = src.getValue(664) - - @Stable - public val kbvCodeDosageFormBan: Translatable = src.getValue(665) - - @Stable - public val kbvCodeDosageFormBeu: Translatable = src.getValue(666) - - @Stable - public val kbvCodeDosageFormBin: Translatable = src.getValue(667) - - @Stable - public val kbvCodeDosageFormBon: Translatable = src.getValue(668) - - @Stable - public val kbvCodeDosageFormBpl: Translatable = src.getValue(669) - - @Stable - public val kbvCodeDosageFormBre: Translatable = src.getValue(670) - - @Stable - public val kbvCodeDosageFormBta: Translatable = src.getValue(671) - - @Stable - public val kbvCodeDosageFormCre: Translatable = src.getValue(672) - - @Stable - public val kbvCodeDosageFormDfl: Translatable = src.getValue(673) - - @Stable - public val kbvCodeDosageFormDil: Translatable = src.getValue(674) - - @Stable - public val kbvCodeDosageFormDis: Translatable = src.getValue(675) - - @Stable - public val kbvCodeDosageFormDka: Translatable = src.getValue(676) - - @Stable - public val kbvCodeDosageFormDos: Translatable = src.getValue(677) - - @Stable - public val kbvCodeDosageFormDra: Translatable = src.getValue(678) - - @Stable - public val kbvCodeDosageFormDrm: Translatable = src.getValue(679) - - @Stable - public val kbvCodeDosageFormDsc: Translatable = src.getValue(680) - - @Stable - public val kbvCodeDosageFormDss: Translatable = src.getValue(681) - - @Stable - public val kbvCodeDosageFormEdp: Translatable = src.getValue(682) - - @Stable - public val kbvCodeDosageFormEin: Translatable = src.getValue(683) - - @Stable - public val kbvCodeDosageFormEle: Translatable = src.getValue(684) - - @Stable - public val kbvCodeDosageFormEli: Translatable = src.getValue(685) - - @Stable - public val kbvCodeDosageFormEmu: Translatable = src.getValue(686) - - @Stable - public val kbvCodeDosageFormEss: Translatable = src.getValue(687) - - @Stable - public val kbvCodeDosageFormEsu: Translatable = src.getValue(688) - - @Stable - public val kbvCodeDosageFormExt: Translatable = src.getValue(689) - - @Stable - public val kbvCodeDosageFormFbe: Translatable = src.getValue(690) - - @Stable - public val kbvCodeDosageFormFbw: Translatable = src.getValue(691) - - @Stable - public val kbvCodeDosageFormFda: Translatable = src.getValue(692) - - @Stable - public val kbvCodeDosageFormFer: Translatable = src.getValue(693) - - @Stable - public val kbvCodeDosageFormFet: Translatable = src.getValue(694) - - @Stable - public val kbvCodeDosageFormFla: Translatable = src.getValue(695) - - @Stable - public val kbvCodeDosageFormFle: Translatable = src.getValue(696) - - @Stable - public val kbvCodeDosageFormFlu: Translatable = src.getValue(697) - - @Stable - public val kbvCodeDosageFormFmr: Translatable = src.getValue(698) - - @Stable - public val kbvCodeDosageFormFol: Translatable = src.getValue(699) - - @Stable - public val kbvCodeDosageFormFrb: Translatable = src.getValue(700) - - @Stable - public val kbvCodeDosageFormFse: Translatable = src.getValue(701) - - @Stable - public val kbvCodeDosageFormFta: Translatable = src.getValue(702) - - @Stable - public val kbvCodeDosageFormGek: Translatable = src.getValue(703) - - @Stable - public val kbvCodeDosageFormGel: Translatable = src.getValue(704) - - @Stable - public val kbvCodeDosageFormGli: Translatable = src.getValue(705) - - @Stable - public val kbvCodeDosageFormGlo: Translatable = src.getValue(706) - - @Stable - public val kbvCodeDosageFormGmr: Translatable = src.getValue(707) - - @Stable - public val kbvCodeDosageFormGpa: Translatable = src.getValue(708) - - @Stable - public val kbvCodeDosageFormGra: Translatable = src.getValue(709) - - @Stable - public val kbvCodeDosageFormGse: Translatable = src.getValue(710) - - @Stable - public val kbvCodeDosageFormGul: Translatable = src.getValue(711) - - @Stable - public val kbvCodeDosageFormHas: Translatable = src.getValue(712) - - @Stable - public val kbvCodeDosageFormHkm: Translatable = src.getValue(713) - - @Stable - public val kbvCodeDosageFormHkp: Translatable = src.getValue(714) - - @Stable - public val kbvCodeDosageFormHpi: Translatable = src.getValue(715) - - @Stable - public val kbvCodeDosageFormHvw: Translatable = src.getValue(716) - - @Stable - public val kbvCodeDosageFormIfa: Translatable = src.getValue(717) - - @Stable - public val kbvCodeDosageFormIfb: Translatable = src.getValue(718) - - @Stable - public val kbvCodeDosageFormIfd: Translatable = src.getValue(719) - - @Stable - public val kbvCodeDosageFormIfe: Translatable = src.getValue(720) - - @Stable - public val kbvCodeDosageFormIff: Translatable = src.getValue(721) - - @Stable - public val kbvCodeDosageFormIfk: Translatable = src.getValue(722) - - @Stable - public val kbvCodeDosageFormIfl: Translatable = src.getValue(723) - - @Stable - public val kbvCodeDosageFormIfs: Translatable = src.getValue(724) - - @Stable - public val kbvCodeDosageFormIha: Translatable = src.getValue(725) - - @Stable - public val kbvCodeDosageFormIhp: Translatable = src.getValue(726) - - @Stable - public val kbvCodeDosageFormIie: Translatable = src.getValue(727) - - @Stable - public val kbvCodeDosageFormIil: Translatable = src.getValue(728) - - @Stable - public val kbvCodeDosageFormIim: Translatable = src.getValue(729) - - @Stable - public val kbvCodeDosageFormIka: Translatable = src.getValue(730) - - @Stable - public val kbvCodeDosageFormIlo: Translatable = src.getValue(731) - - @Stable - public val kbvCodeDosageFormImp: Translatable = src.getValue(732) - - @Stable - public val kbvCodeDosageFormInf: Translatable = src.getValue(733) - - @Stable - public val kbvCodeDosageFormInh: Translatable = src.getValue(734) - - @Stable - public val kbvCodeDosageFormIni: Translatable = src.getValue(735) - - @Stable - public val kbvCodeDosageFormInl: Translatable = src.getValue(736) - - @Stable - public val kbvCodeDosageFormIns: Translatable = src.getValue(737) - - @Stable - public val kbvCodeDosageFormIst: Translatable = src.getValue(738) - - @Stable - public val kbvCodeDosageFormIsu: Translatable = src.getValue(739) - - @Stable - public val kbvCodeDosageFormIup: Translatable = src.getValue(740) - - @Stable - public val kbvCodeDosageFormKan: Translatable = src.getValue(741) - - @Stable - public val kbvCodeDosageFormKap: Translatable = src.getValue(742) - - @Stable - public val kbvCodeDosageFormKat: Translatable = src.getValue(743) - - @Stable - public val kbvCodeDosageFormKda: Translatable = src.getValue(744) - - @Stable - public val kbvCodeDosageFormKeg: Translatable = src.getValue(745) - - @Stable - public val kbvCodeDosageFormKer: Translatable = src.getValue(746) - - @Stable - public val kbvCodeDosageFormKgu: Translatable = src.getValue(747) - - @Stable - public val kbvCodeDosageFormKid: Translatable = src.getValue(748) - - @Stable - public val kbvCodeDosageFormKii: Translatable = src.getValue(749) - - @Stable - public val kbvCodeDosageFormKks: Translatable = src.getValue(750) - - @Stable - public val kbvCodeDosageFormKli: Translatable = src.getValue(751) - - @Stable - public val kbvCodeDosageFormKlt: Translatable = src.getValue(752) - - @Stable - public val kbvCodeDosageFormKmp: Translatable = src.getValue(753) - - @Stable - public val kbvCodeDosageFormKmr: Translatable = src.getValue(754) - - @Stable - public val kbvCodeDosageFormKod: Translatable = src.getValue(755) - - @Stable - public val kbvCodeDosageFormKom: Translatable = src.getValue(756) - - @Stable - public val kbvCodeDosageFormKon: Translatable = src.getValue(757) - - @Stable - public val kbvCodeDosageFormKpg: Translatable = src.getValue(758) - - @Stable - public val kbvCodeDosageFormKri: Translatable = src.getValue(759) - - @Stable - public val kbvCodeDosageFormKss: Translatable = src.getValue(760) - - @Stable - public val kbvCodeDosageFormKsu: Translatable = src.getValue(761) - - @Stable - public val kbvCodeDosageFormKta: Translatable = src.getValue(762) - - @Stable - public val kbvCodeDosageFormLan: Translatable = src.getValue(763) - - @Stable - public val kbvCodeDosageFormLii: Translatable = src.getValue(764) - - @Stable - public val kbvCodeDosageFormLiq: Translatable = src.getValue(765) - - @Stable - public val kbvCodeDosageFormLoe: Translatable = src.getValue(766) - - @Stable - public val kbvCodeDosageFormLot: Translatable = src.getValue(767) - - @Stable - public val kbvCodeDosageFormLov: Translatable = src.getValue(768) - - @Stable - public val kbvCodeDosageFormLse: Translatable = src.getValue(769) - - @Stable - public val kbvCodeDosageFormLta: Translatable = src.getValue(770) - - @Stable - public val kbvCodeDosageFormLup: Translatable = src.getValue(771) - - @Stable - public val kbvCodeDosageFormLut: Translatable = src.getValue(772) - - @Stable - public val kbvCodeDosageFormMil: Translatable = src.getValue(773) - - @Stable - public val kbvCodeDosageFormMis: Translatable = src.getValue(774) - - @Stable - public val kbvCodeDosageFormMix: Translatable = src.getValue(775) - - @Stable - public val kbvCodeDosageFormMrg: Translatable = src.getValue(776) - - @Stable - public val kbvCodeDosageFormMrp: Translatable = src.getValue(777) - - @Stable - public val kbvCodeDosageFormMta: Translatable = src.getValue(778) - - @Stable - public val kbvCodeDosageFormMuw: Translatable = src.getValue(779) - - @Stable - public val kbvCodeDosageFormNag: Translatable = src.getValue(780) - - @Stable - public val kbvCodeDosageFormNao: Translatable = src.getValue(781) - - @Stable - public val kbvCodeDosageFormNas: Translatable = src.getValue(782) - - @Stable - public val kbvCodeDosageFormNaw: Translatable = src.getValue(783) - - @Stable - public val kbvCodeDosageFormNds: Translatable = src.getValue(784) - - @Stable - public val kbvCodeDosageFormNsa: Translatable = src.getValue(785) - - @Stable - public val kbvCodeDosageFormNtr: Translatable = src.getValue(786) - - @Stable - public val kbvCodeDosageFormOcu: Translatable = src.getValue(787) - - @Stable - public val kbvCodeDosageFormOel: Translatable = src.getValue(788) - - @Stable - public val kbvCodeDosageFormOht: Translatable = src.getValue(789) - - @Stable - public val kbvCodeDosageFormOvu: Translatable = src.getValue(790) - - @Stable - public val kbvCodeDosageFormPam: Translatable = src.getValue(791) - - @Stable - public val kbvCodeDosageFormPas: Translatable = src.getValue(792) - - @Stable - public val kbvCodeDosageFormPel: Translatable = src.getValue(793) - - @Stable - public val kbvCodeDosageFormPen: Translatable = src.getValue(794) - - @Stable - public val kbvCodeDosageFormPer: Translatable = src.getValue(795) - - @Stable - public val kbvCodeDosageFormPfl: Translatable = src.getValue(796) - - @Stable - public val kbvCodeDosageFormPft: Translatable = src.getValue(797) - - @Stable - public val kbvCodeDosageFormPhi: Translatable = src.getValue(798) - - @Stable - public val kbvCodeDosageFormPhv: Translatable = src.getValue(799) - - @Stable - public val kbvCodeDosageFormPie: Translatable = src.getValue(800) - - @Stable - public val kbvCodeDosageFormPif: Translatable = src.getValue(801) - - @Stable - public val kbvCodeDosageFormPii: Translatable = src.getValue(802) - - @Stable - public val kbvCodeDosageFormPij: Translatable = src.getValue(803) - - @Stable - public val kbvCodeDosageFormPik: Translatable = src.getValue(804) - - @Stable - public val kbvCodeDosageFormPis: Translatable = src.getValue(805) - - @Stable - public val kbvCodeDosageFormPiv: Translatable = src.getValue(806) - - @Stable - public val kbvCodeDosageFormPki: Translatable = src.getValue(807) - - @Stable - public val kbvCodeDosageFormPle: Translatable = src.getValue(808) - - @Stable - public val kbvCodeDosageFormPlf: Translatable = src.getValue(809) - - @Stable - public val kbvCodeDosageFormPlg: Translatable = src.getValue(810) - - @Stable - public val kbvCodeDosageFormPlh: Translatable = src.getValue(811) - - @Stable - public val kbvCodeDosageFormPli: Translatable = src.getValue(812) - - @Stable - public val kbvCodeDosageFormPlk: Translatable = src.getValue(813) - - @Stable - public val kbvCodeDosageFormPls: Translatable = src.getValue(814) - - @Stable - public val kbvCodeDosageFormPlv: Translatable = src.getValue(815) - - @Stable - public val kbvCodeDosageFormPpl: Translatable = src.getValue(816) - - @Stable - public val kbvCodeDosageFormPrs: Translatable = src.getValue(817) - - @Stable - public val kbvCodeDosageFormPse: Translatable = src.getValue(818) - - @Stable - public val kbvCodeDosageFormPst: Translatable = src.getValue(819) - - @Stable - public val kbvCodeDosageFormPud: Translatable = src.getValue(820) - - @Stable - public val kbvCodeDosageFormPul: Translatable = src.getValue(821) - - @Stable - public val kbvCodeDosageFormRed: Translatable = src.getValue(822) - - @Stable - public val kbvCodeDosageFormRek: Translatable = src.getValue(823) - - @Stable - public val kbvCodeDosageFormRet: Translatable = src.getValue(824) - - @Stable - public val kbvCodeDosageFormRgr: Translatable = src.getValue(825) - - @Stable - public val kbvCodeDosageFormRka: Translatable = src.getValue(826) - - @Stable - public val kbvCodeDosageFormRms: Translatable = src.getValue(827) - - @Stable - public val kbvCodeDosageFormRsc: Translatable = src.getValue(828) - - @Stable - public val kbvCodeDosageFormRsu: Translatable = src.getValue(829) - - @Stable - public val kbvCodeDosageFormRut: Translatable = src.getValue(830) - - @Stable - public val kbvCodeDosageFormSaf: Translatable = src.getValue(831) - - @Stable - public val kbvCodeDosageFormSal: Translatable = src.getValue(832) - - @Stable - public val kbvCodeDosageFormSam: Translatable = src.getValue(833) - - @Stable - public val kbvCodeDosageFormSch: Translatable = src.getValue(834) - - @Stable - public val kbvCodeDosageFormSei: Translatable = src.getValue(835) - - @Stable - public val kbvCodeDosageFormSha: Translatable = src.getValue(836) - - @Stable - public val kbvCodeDosageFormSir: Translatable = src.getValue(837) - - @Stable - public val kbvCodeDosageFormSlz: Translatable = src.getValue(838) - - @Stable - public val kbvCodeDosageFormSmf: Translatable = src.getValue(839) - - @Stable - public val kbvCodeDosageFormSmt: Translatable = src.getValue(840) - - @Stable - public val kbvCodeDosageFormSmu: Translatable = src.getValue(841) - - @Stable - public val kbvCodeDosageFormSpa: Translatable = src.getValue(842) - - @Stable - public val kbvCodeDosageFormSpf: Translatable = src.getValue(843) - - @Stable - public val kbvCodeDosageFormSpl: Translatable = src.getValue(844) - - @Stable - public val kbvCodeDosageFormSpr: Translatable = src.getValue(845) - - @Stable - public val kbvCodeDosageFormSpt: Translatable = src.getValue(846) - - @Stable - public val kbvCodeDosageFormSri: Translatable = src.getValue(847) - - @Stable - public val kbvCodeDosageFormSsu: Translatable = src.getValue(848) - - @Stable - public val kbvCodeDosageFormSta: Translatable = src.getValue(849) - - @Stable - public val kbvCodeDosageFormStb: Translatable = src.getValue(850) - - @Stable - public val kbvCodeDosageFormSti: Translatable = src.getValue(851) - - @Stable - public val kbvCodeDosageFormStr: Translatable = src.getValue(852) - - @Stable - public val kbvCodeDosageFormSub: Translatable = src.getValue(853) - - @Stable - public val kbvCodeDosageFormSue: Translatable = src.getValue(854) - - @Stable - public val kbvCodeDosageFormSul: Translatable = src.getValue(855) - - @Stable - public val kbvCodeDosageFormSup: Translatable = src.getValue(856) - - @Stable - public val kbvCodeDosageFormSus: Translatable = src.getValue(857) - - @Stable - public val kbvCodeDosageFormSut: Translatable = src.getValue(858) - - @Stable - public val kbvCodeDosageFormSuv: Translatable = src.getValue(859) - - @Stable - public val kbvCodeDosageFormSwa: Translatable = src.getValue(860) - - @Stable - public val kbvCodeDosageFormTab: Translatable = src.getValue(861) - - @Stable - public val kbvCodeDosageFormTae: Translatable = src.getValue(862) - - @Stable - public val kbvCodeDosageFormTam: Translatable = src.getValue(863) - - @Stable - public val kbvCodeDosageFormTee: Translatable = src.getValue(864) - - @Stable - public val kbvCodeDosageFormTei: Translatable = src.getValue(865) - - @Stable - public val kbvCodeDosageFormTes: Translatable = src.getValue(866) - - @Stable - public val kbvCodeDosageFormTin: Translatable = src.getValue(867) - - @Stable - public val kbvCodeDosageFormTka: Translatable = src.getValue(868) - - @Stable - public val kbvCodeDosageFormTle: Translatable = src.getValue(869) - - @Stable - public val kbvCodeDosageFormTmr: Translatable = src.getValue(870) - - @Stable - public val kbvCodeDosageFormTon: Translatable = src.getValue(871) - - @Stable - public val kbvCodeDosageFormTpn: Translatable = src.getValue(872) - - @Stable - public val kbvCodeDosageFormTpo: Translatable = src.getValue(873) - - @Stable - public val kbvCodeDosageFormTra: Translatable = src.getValue(874) - - @Stable - public val kbvCodeDosageFormTri: Translatable = src.getValue(875) - - @Stable - public val kbvCodeDosageFormTro: Translatable = src.getValue(876) - - @Stable - public val kbvCodeDosageFormTrs: Translatable = src.getValue(877) - - @Stable - public val kbvCodeDosageFormTrt: Translatable = src.getValue(878) - - @Stable - public val kbvCodeDosageFormTsa: Translatable = src.getValue(879) - - @Stable - public val kbvCodeDosageFormTsd: Translatable = src.getValue(880) - - @Stable - public val kbvCodeDosageFormTse: Translatable = src.getValue(881) - - @Stable - public val kbvCodeDosageFormTss: Translatable = src.getValue(882) - - @Stable - public val kbvCodeDosageFormTst: Translatable = src.getValue(883) - - @Stable - public val kbvCodeDosageFormTsy: Translatable = src.getValue(884) - - @Stable - public val kbvCodeDosageFormTtr: Translatable = src.getValue(885) - - @Stable - public val kbvCodeDosageFormTub: Translatable = src.getValue(886) - - @Stable - public val kbvCodeDosageFormTue: Translatable = src.getValue(887) - - @Stable - public val kbvCodeDosageFormTup: Translatable = src.getValue(888) - - @Stable - public val kbvCodeDosageFormTvw: Translatable = src.getValue(889) - - @Stable - public val kbvCodeDosageFormUta: Translatable = src.getValue(890) - - @Stable - public val kbvCodeDosageFormVal: Translatable = src.getValue(891) - - @Stable - public val kbvCodeDosageFormVar: Translatable = src.getValue(892) - - @Stable - public val kbvCodeDosageFormVcr: Translatable = src.getValue(893) - - @Stable - public val kbvCodeDosageFormVer: Translatable = src.getValue(894) - - @Stable - public val kbvCodeDosageFormVge: Translatable = src.getValue(895) - - @Stable - public val kbvCodeDosageFormVka: Translatable = src.getValue(896) - - @Stable - public val kbvCodeDosageFormVli: Translatable = src.getValue(897) - - @Stable - public val kbvCodeDosageFormVov: Translatable = src.getValue(898) - - @Stable - public val kbvCodeDosageFormVst: Translatable = src.getValue(899) - - @Stable - public val kbvCodeDosageFormVsu: Translatable = src.getValue(900) - - @Stable - public val kbvCodeDosageFormVta: Translatable = src.getValue(901) - - @Stable - public val kbvCodeDosageFormWat: Translatable = src.getValue(902) - - @Stable - public val kbvCodeDosageFormWga: Translatable = src.getValue(903) - - @Stable - public val kbvCodeDosageFormWka: Translatable = src.getValue(904) - - @Stable - public val kbvCodeDosageFormWkm: Translatable = src.getValue(905) - - @Stable - public val kbvCodeDosageFormWue: Translatable = src.getValue(906) - - @Stable - public val kbvCodeDosageFormXdg: Translatable = src.getValue(907) - - @Stable - public val kbvCodeDosageFormXds: Translatable = src.getValue(908) - - @Stable - public val kbvCodeDosageFormXfe: Translatable = src.getValue(909) - - @Stable - public val kbvCodeDosageFormXgm: Translatable = src.getValue(910) - - @Stable - public val kbvCodeDosageFormXha: Translatable = src.getValue(911) - - @Stable - public val kbvCodeDosageFormXhs: Translatable = src.getValue(912) - - @Stable - public val kbvCodeDosageFormXnc: Translatable = src.getValue(913) - - @Stable - public val kbvCodeDosageFormXpk: Translatable = src.getValue(914) - - @Stable - public val kbvCodeDosageFormXtc: Translatable = src.getValue(915) - - @Stable - public val kbvCodeDosageFormZam: Translatable = src.getValue(916) - - @Stable - public val kbvCodeDosageFormZbu: Translatable = src.getValue(917) - - @Stable - public val kbvCodeDosageFormZcr: Translatable = src.getValue(918) - - @Stable - public val kbvCodeDosageFormZge: Translatable = src.getValue(919) - - @Stable - public val kbvCodeDosageFormZka: Translatable = src.getValue(920) - - @Stable - public val kbvCodeDosageFormZpa: Translatable = src.getValue(921) -} - -public val stringsDe: Strings = Strings( - mapOf( - 0 to Singular("E-Rezept"), - 1 to Singular("Okay"), - 2 to Singular("Verstanden"), - 3 to Singular("Abbrechen"), - 4 to Singular("Zurück"), - 5 to Singular("um"), - 6 to Singular("%1\$s Uhr"), - 7 to Singular("Zuletzt aktualisiert am %1\$s"), - 8 to Singular("Aktualisierung fehlgeschlagen. Bitte aktualisieren Sie Ihre Rezepte erneut."), - 9 to Singular("Digital. Schnell. Sicher."), - 10 to Singular("erhalten. einlösen. verwalten."), - 11 to Singular("Willkommen in der E-Rezept-App"), - 12 to - Singular("Hier können Sie elektronische Rezepte in einer Apotheke Ihrer Wahl einlösen, direkt vor Ort oder online."), - 13 to Singular("Mehr Funktionen mit Ihrer Gesundheitskarte"), - 14 to Singular("Aktualisieren Sie automatisch Ihre neuen Rezepte"), - 15 to Singular("Informationen zur Einnahme und Dosierungen Ihrer Medikamente"), - 16 to Singular("Empfangen Sie Mitteilungen Ihrer Apotheke zu Ihrer Bestellung"), - 17 to Singular("Nutzungsbedingungen & Datenschutzerklärung"), - 18 to - Singular("Um die App nutzen zu können, stimmen Sie bitte den Nutzungsbedingungen zu und bestätigen Sie die Kenntnisnahme der Datenschutzbedingungen. Es werden nur Daten erfasst, die für das Funktionieren der Dienste unerlässlich sind."), - 19 to Singular("Ich habe die %s gelesen und akzeptiere sie."), - 20 to Singular("Nutzungsbedingungen"), - 21 to Singular("Datenschutzerklärung"), - 22 to Singular("Bestätigen"), - 23 to Singular("Weiter"), - 24 to Singular("Bestätigen"), - 25 to Singular("Rezepte hinzufügen"), - 26 to - Singular("Sie haben einen Rezept-Ausdruck erhalten? Rezepte fügen Sie der App hinzu, indem Sie den jeweiligen Rezeptcode abscannen."), - 27 to Singular("Verstanden"), - 28 to Singular("Task-ID"), - 29 to Singular("Access-Code"), - 30 to Singular("Kopiert"), - 31 to Singular("Wiederholtes Passwort stimmt überein"), - 32 to Singular("Nutzungsbedingungen"), - 33 to Singular("Datenschutzerklärung"), - 34 to Singular("Nutzungsbedingungen akzeptieren"), - 35 to Singular("Datenschutzerklärung akzeptieren"), - 36 to Singular("Rezepte"), - 37 to Singular("Rezepte"), - 38 to Singular("Mitteilungen"), - 39 to Singular("Apotheken"), - 40 to Singular("Einlösen"), - 41 to Plurals( - mapOf( - Plurals.Type.One to "Noch %s Tag als Selbstzahlender einlösbar", - Plurals.Type.Other to "Noch %s Tage als Selbstzahlender einlösbar" - ) - ), - 42 to Plurals( - mapOf( - Plurals.Type.One to "Noch %s Tag gültig", - Plurals.Type.Other to - "Noch %s Tage gültig" - ) - ), - 43 to Singular("z. B. Hautärztin"), - 44 to Singular("%s %s aus %s %s erkannt. Weitere Codes scannen?"), - 45 to Plurals(mapOf(Plurals.Type.One to "Rezept", Plurals.Type.Other to "Rezepten")), - 46 to Plurals(mapOf(Plurals.Type.One to "Rezept", Plurals.Type.Other to "Rezepte")), - 47 to Plurals( - mapOf( - Plurals.Type.One to "%s Rezept hinzufügen", - Plurals.Type.Other to - "%s Rezepte hinzufügen" - ) - ), - 48 to Singular("Zugriff auf Kamera verweigert"), - 49 to - Singular("Um den Scanner verwenden zu können, müssen Sie der App in den Systemeinstellungen den Zugriff auf Ihre Kamera gestatten."), - 50 to Singular("Fokussieren Sie mit der Kamera auf einen Rezeptcode"), - 51 to Singular("Hierbei handelt es sich um keinen gültigen Rezeptcode"), - 52 to Singular("Dieser Rezeptcode wurde bereits abgescannt"), - 53 to Plurals( - mapOf( - Plurals.Type.One to "%s Rezept erkannt", - Plurals.Type.Other to - "%s Rezepte erkannt" - ) - ), - 54 to Singular("Abbrechen"), - 55 to Singular("Kameralicht"), - 56 to Singular("Scannen von Rezeptcodes abbrechen?"), - 57 to Singular("Scannen abbrechen"), - 58 to Singular("Fortfahren"), - 59 to Singular("Scanner öffnen"), - 60 to - Singular("Zum Lesen des Rezeptcodes nutzt diese App einen Service von Google. Dabei werden Daten an Google übertragen."), - 61 to Singular("Einverstanden"), - 62 to Singular("Abbrechen"), - 63 to Singular("Karte hinzufügen"), - 64 to Singular("Los geht’s"), - 65 to Singular("Jetzt alle Funktionen nutzen"), - 66 to - Singular("Um alle Funktionen der App nutzen zu können, melden Sie sich mit Ihrer Gesundheitskarte an. Diese Karte sowie die benötigen Zugangsdaten erhalten Sie von Ihrer Krankenversicherung."), - 67 to Singular("Was Sie benötigen:"), - 68 to - Singular("Wie erhalte ich eine neue Gesundheitskarte? Hier hilft Ihnen Ihre Krankenversicherung."), - 69 to Singular("Eine Gesundheitskarte mit Zugangsnummer (CAN)"), - 70 to Singular("Die PIN zur Gesundheitskarte"), - 71 to - Singular("Ihre Kartenzugangsnummer (Card Access Number, kurz: CAN) hat 6 Stellen. Sie finden die CAN in der rechten oberen Ecke der Vorderseite Ihrer Gesundheitskarte. Steht hier keine sechsstellige Zugangsnummer, benötigen Sie eine neue Gesundheitskarte von Ihrer Krankenversicherung."), - 72 to Singular("Zugangsnummer eingeben"), - 73 to Singular("Sie können beliebige Ziffern angeben."), - 74 to Singular("Ihre PIN kann 6 bis 8 Stellen haben."), - 75 to Singular("PIN eingeben"), - 76 to Singular("Im Demo-Modus können Sie eine beliebige PIN eingeben."), - 77 to Singular("Erneut probieren"), - 78 to Singular("Halten Sie nun Ihre elektronische Gesundheitskarte bereit."), - 79 to - Singular("Die Verbindung Ihres Geräts mit dem Server kann je nach Hardware und Internetgeschwindigkeit unterschiedlich lange dauern."), - 80 to Singular("Verbindung mit dem Server herstellen fehlgeschlagen."), - 81 to - Singular("Überprüfen Sie Ihre Verbindung mit dem Internet und starten Sie den Vorgang erneut."), - 82 to Singular("Fehler 20 10 76631"), - 83 to - Singular("Das Zertifikat Ihrer Gesundheitskarte ist ungültig. Ist Ihre Karte möglicherweise abgelaufen? Bitte kontaktieren Sie Ihre Krankenkasse."), - 84 to Singular("Falsche PIN eingegeben."), - 85 to Plurals( - mapOf( - Plurals.Type.One to - "Sie haben noch %s weiteren Versuch, bevor Ihre Karte gesperrt wird.", - Plurals.Type.Other to - "Sie haben noch %s weitere Versuche, bevor Ihre Karte gesperrt wird." - ) - ), - 86 to Singular("Falsche CAN eingegeben"), - 87 to Singular("Sie finden die Zugangsnummer oben rechts auf Ihrer Gesundheitskarte."), - 88 to Singular("PIN wurde mehrmals falsch eingegeben."), - 89 to Singular("Ihre Gesundheitskarte muss mit der PUK entsperrt werden."), - 90 to Singular("Abbrechen"), - 91 to Singular("Suche nach Karte…"), - 92 to Singular("Halten Sie die Gesundheitskarte an die Rückseite Ihres Geräts."), - 93 to Singular("Immer noch auf der Suche …"), - 94 to Singular("Bewegen Sie langsam die Karte an der Rückseite des Geräts."), - 95 to Singular("Tipp"), - 96 to Singular("Gerätehüllen können ggf. die Verbindung über NFC erschweren."), - 97 to Singular("Karte erkannt"), - 98 to Singular("Versuchen Sie, die Gesundheitskarte nicht zu bewegen."), - 99 to Singular("25%"), - 100 to Singular("50%"), - 101 to Singular("75%"), - 102 to Singular("100%"), - 103 to Singular("Gesundheitskarte gefunden. Bitte nicht bewegen."), - 104 to Singular("Verbindung abgebrochen"), - 105 to Singular("Halten Sie Ihre Gesundheitskarte erneut an die Rückseite des Geräts"), - 106 to Singular("Sie haben sich erfolgreich angemeldet"), - 107 to - Singular("Hinweis: es werden nur die Rezepte aus den letzten 100 Tagen heruntergeladen."), - 108 to Singular("Demo-Modus aktiviert"), - 109 to - Singular("Sie haben eine NFC-fähige Gesundheitskarte und möchten diese im Demo-Modus ausprobieren?"), - 110 to Singular("Weiter mit Karte"), - 111 to Singular("Weiter ohne Karte"), - 112 to Singular("Demo-Modus aktiviert"), - 113 to Singular("Version: %s"), - 114 to Singular("Build-Hash: %s"), - 115 to Singular("Debug-Menü"), - 116 to Singular("Rezeptcode"), - 117 to Singular("Lassen Sie diesen Rezeptcode in Ihrer Apotheke abscannen."), - 118 to Singular("Dieser Sammelcode bündelt %s Rezepte"), - 119 to Singular("In Apotheke einlösen"), - 120 to Singular("Sie stehen in einer Apotheke und möchten Ihr Rezept einlösen."), - 121 to Singular("Bestellen oder reservieren"), - 122 to - Singular("Senden Sie Ihr Rezept an eine Apotheke und entscheiden Sie, wie Sie Ihre Medikamente erhalten möchten."), - 123 to Singular("Hierfür benötigen Sie eine gültige Gesundheitskarte."), - 124 to Singular("Apotheke wählen"), - 125 to Singular("z. B. Pinguin Apotheke oder Adresse"), - 126 to Singular("Leicht Apotheken finden"), - 127 to Singular("Standort freigeben und Apotheken in Ihrer Umgebung finden"), - 128 to Singular("Standort freigeben"), - 129 to Singular("Geöffnet bis %s Uhr"), - 130 to Singular("Durchgehend geöffnet"), - 131 to Singular("Impressum"), - 132 to Singular("Herausgeber"), - 133 to Singular("gematik GmbH\nFriedrichstraße 136\n10117 Berlin"), - 134 to - Singular("Geschäftsführer: Dr. med. Markus Leyck Dieken\nRegistergericht: Amtsgericht Berlin-Charlottenburg\nHandelsregister-Nr.: HRB 96351\nUmsatzsteueridentifikationsnummer: DE241843684"), - 135 to Singular("Verantwortlich für den Inhalt"), - 136 to Singular("Dr. med. Markus Leyck Dieken"), - 137 to Singular("Kontakt"), - 138 to Singular("https://www.das-e-rezept-fuer-deutschland.de/kontakt"), - 139 to Singular("app-feedback@gematik.de"), - 140 to Singular("Hinweis"), - 141 to - Singular("Wir bemühen uns um eine geschlechtergerechte Sprache. Sollten Ihnen Fehler auffallen, freuen wir uns über eine Mitteilung per Mail."), - 142 to Singular("Eingescanntes Rezept"), - 143 to Singular("Medikament %s"), - 144 to Singular("Aktuell"), - 145 to Singular("Aktualisieren"), - 146 to Singular("Archiv"), - 147 to Singular("Sie haben noch keine Rezepte eingelöst"), - 148 to Plurals( - mapOf( - Plurals.Type.One to "%s Medikament", - Plurals.Type.Other to - "%s Medikamente" - ) - ), - 149 to Singular("Eingelöst am %s"), - 150 to Singular("Sie haben noch keine Rezepte eingelöst"), - 151 to Singular("Deutschlands moderne Plattform für digitale Medizin"), - 152 to Singular("Mail schreiben"), - 153 to Singular("Webseite öffnen"), - 154 to Singular("Willkommen"), - 155 to Singular("Anmeldung starten"), - 156 to Singular("Auf Entsperren drücken"), - 157 to Singular("Das hat leider nicht geklappt \uD83D\uDE15"), - 158 to Singular("Bitte probieren Sie es erneut."), - 159 to Singular("Entsperren"), - 160 to - Singular("Sie haben Fragen oder Probleme bei der Nutzung der App? Unsere technische Hotline erreichen Sie unter %s."), - 161 to Singular("Viele Fragen haben wir bereits auf %s für Sie beantwortet."), - 162 to Singular("Anmelden"), - 163 to Singular("Abbrechen"), - 164 to Singular("https://www.das-e-rezept-fuer-deutschland.de/"), - 165 to Singular("das-e-rezept-fuer-deutschland.de"), - 166 to Singular("Kennwort eingeben"), - 167 to Singular("Weiter"), - 168 to Singular("Erfolglose Anmeldeversuche"), - 169 to Plurals( - mapOf( - Plurals.Type.One to - "Es wurde %s erfolgloser Anmeldeversuche festgestellt.", - Plurals.Type.Other to - "Es wurden %s erfolglose Anmeldeversuche festgestellt." - ) - ), - 170 to Singular("Einstellungen"), - 171 to Singular("Name unbekannt"), - 172 to Singular("Gesundheitskarten"), - 173 to Singular("Karte hinzufügen"), - 174 to Singular("Zum Ausprobieren"), - 175 to - Singular("Der Demo-Modus erlaubt es Ihnen, alle Bereiche der App auch ohne elektronische Gesundheitskarte zu erkunden."), - 176 to Singular("Demo-Modus"), - 177 to Singular("Bedienungshilfen"), - 178 to Singular("Zoomen"), - 179 to - Singular("Ermöglicht das Vergrößern der App über das Zusammen- oder Auseinanderziehen der Finger (Pinch-to-Zoom)."), - 180 to Singular("Sicherheit"), - 181 to Singular("Schützen Sie Ihre Gesundheitsinformationen vor dem Zugriff Unbefugter."), - 182 to Singular("Beste Gerätesicherung wählen"), - 183 to Singular("Hierbei kann es sich um Fingerabdruck, Wischmuster oder ähnliches handeln"), - 184 to - Singular("Die beste verfügbare Geräteabsicherung wurde nicht eingerichtet. Hierbei kann es sich um Fingerabdruck, Wischmuster oder ähnliches handeln"), - 185 to Singular("Rechtliches"), - 186 to Singular("Impressum"), - 187 to Singular("Datenschutz"), - 188 to Singular("Nutzungsbedingungen"), - 189 to Singular("Tokens"), - 190 to Singular("Access Token"), - 191 to Singular("SSO Token"), - 192 to Singular("Kein Access Token verfügbar"), - 193 to Singular("kein SSO Token verfügbar"), - 194 to Singular("in die Zwischenablage kopiert"), - 195 to Singular("Klicken, um den Token in die Zwischenablage zu koperen"), - 196 to Singular("Demo-Modus aktiviert"), - 197 to - Singular("Unser Demo-Modus zeigt Ihnen alle Funktionen der App – ganz ohne Gesundheitskarte."), - 198 to Singular("Erkundungstour gefällig?"), - 199 to - Singular("Unser Demo-Modus zeigt Ihnen alle Funktionen der App – ganz ohne Gesundheitskarte."), - 200 to Singular("Demo-Modus starten"), - 201 to Singular("Sie haben keine aktuellen Rezepte"), - 202 to Singular("Rezeptdaten absichern"), - 203 to Singular("Verbesserter Schutz Ihrer Daten durch Fingerabdruck oder Gesichts-Scan."), - 204 to Singular("Jetzt aktivieren"), - 205 to Singular("Details"), - 206 to Singular("Behalten Sie den Überblick"), - 207 to - Singular("Markieren Sie dieses Rezept als eingelöst, sobald Sie Ihr Medikament erhalten haben."), - 208 to Singular("Rezepte automatisch aktualisieren"), - 209 to - Singular("Melden Sie sich an, damit Ihre Rezepte automatisch als eingelöst markiert werden können."), - 210 to Singular("Jetzt anmelden"), - 211 to Singular("Weshalb sehe ich nur diese Informationen?"), - 212 to Singular("Ihre Gesundheitsinformationen genießen besonderen Schutz"), - 213 to Singular("Medikament %1\$d"), - 214 to Singular("Als eingelöst markieren"), - 215 to Singular("Als nicht eingelöst markieren"), - 216 to Singular("Von diesem Gerät löschen"), - 217 to Singular("Protokoll"), - 218 to Singular("Gescannt am"), - 219 to Singular("%1\$s Uhr"), - 220 to Singular("Noch einlösbar bis %s"), - 221 to Singular("Als Selbstzahler noch einlösbar bis %s"), - 222 to Singular("Details zu diesem Medikament"), - 223 to Singular("Darreichungsform"), - 224 to Singular("Packungsgröße"), - 225 to Singular("Pharmazentralnummer (PZN)"), - 226 to Singular("Einnahmehinweise"), - 227 to - Singular("Bitte beachten Sie die Einnahmehinweise in Ihrem Medikationsplan oder die schriftliche Dosierungsanweisung Ihres Arztes."), - 228 to Singular("Versicherte Person"), - 229 to Singular("Name"), - 230 to Singular("Adresse"), - 231 to Singular("Geburtsdatum"), - 232 to Singular("Krankenversicherung / Kostenträger"), - 233 to Singular("Status"), - 234 to Singular("Versichertennummer"), - 235 to Singular("Verschreibende Person"), - 236 to Singular("Name"), - 237 to Singular("Facharzt / Fachärztin"), - 238 to Singular("Arztnummer (LANR)"), - 239 to Singular("Institution"), - 240 to Singular("Name"), - 241 to Singular("Adresse"), - 242 to Singular("Betriebsstätten-Nummer"), - 243 to Singular("Telefonnummer"), - 244 to Singular("Mail"), - 245 to Singular("Arbeitsunfall"), - 246 to Singular("Unfalltag"), - 247 to Singular("Unfallbetrieb- oder Arbeitgebernummer"), - 248 to Singular("Möchten Sie dieses Rezept unwiderruflich löschen?"), - 249 to Singular("Löschen"), - 250 to Singular("Abbrechen"), - 251 to Singular("Nur dieses Rezept wieder verfügbar machen oder alle?"), - 252 to Singular("Alle"), - 253 to Singular("Nur dieses"), - 254 to Singular("Hier ist Eile geboten"), - 255 to - Singular("Dieses Medikament kann ohne Notdienstgebühr auch nachts in einer Apotheke eingelöst werden."), - 256 to Singular("Ersatzpräparat möglich"), - 257 to - Singular("Ersatzpräparate sind zulässig. Aufgrund gesetzlicher Vorgaben Ihrer Krankenversicherung kann Ihnen eine Alternative ausgehändigt werden."), - 258 to Singular("Verbindlich reservieren"), - 259 to Singular("Botendienst anfragen"), - 260 to Singular("Per Versand liefern lassen"), - 261 to Singular("Hinweis"), - 262 to - Singular("Bitte beachten Sie, dass auch für verschriebene Medikamente Zuzahlungen anfallen können."), - 263 to Singular("Öffnungszeiten"), - 264 to Singular("Webseite"), - 265 to Singular("Reservierung"), - 266 to Singular("Möchten Sie folgende Rezepte in der %s verbindlich einlösen?"), - 267 to Singular("Rezepte"), - 268 to Singular("Einlösen"), - 269 to Singular("Botendienst"), - 270 to Singular("Lieferadresse"), - 271 to Singular("Wie können wir helfen?"), - 272 to - Singular("Hat sich die Lieferadresse geändert? Sie möchten der Apotheke noch etwas mitteilen?"), - 273 to Singular("Jetzt anrufen"), - 274 to Singular("Versand"), - 275 to Singular("Protokoll"), - 276 to Singular("ohne hinterlegte Aktion"), - 277 to Singular("nur noch heute gültig"), - 278 to Singular("Nur noch heute als Selbstzahlender einlösbar"), - 279 to Singular("Nicht mehr gültig"), - 280 to Singular("Rezeptblock umbenennen"), - 281 to Singular("Sie können einen Namen für diesen Rezeptblock vergeben."), - 282 to Singular("Anmelden"), - 283 to Singular("Anmelden"), - 284 to Singular("Ein NFC-fähiges Smartphone mit mindestens Android 7"), - 285 to Singular("NFC aktivieren"), - 286 to - Singular("Bitte aktivieren Sie die NFC-Funktion Ihres Geräts, um sich mit Ihrer Gesundheitskarte anzumelden."), - 287 to Singular("Aktivieren"), - 288 to Singular("Wie erhalte ich eine neue Gesundheitskarte?"), - 289 to Singular("Hier hilft Ihnen Ihre Krankenversicherung."), - 290 to Singular("Wie erhalte ich eine PIN?"), - 291 to - Singular("Eine PIN für Ihre Gesundheitskarte erhalten Sie in einem separaten Brief von Ihrer Krankenversicherung."), - 292 to Singular("Möchten Sie Ihre Zugangsdaten für zukünftige Anmeldungen speichern?"), - 293 to Singular("Zugangsdaten speichern"), - 294 to Singular("Komfortabel: Hierfür werden Ihre Daten biometrisch auf dem Gerät geschützt"), - 295 to Singular("Sicherung nicht möglich"), - 296 to - Singular("Keine sicheren Sensoren verfügbar oder biometrische Sicherung nicht eingerichtet."), - 297 to Singular("Zugangsdaten nicht speichern"), - 298 to - Singular("Datensparsam: Erfordert die Eingabe Ihrer Zugangsdaten bei jedem Start der App"), - 299 to Singular("Korrigieren"), - 300 to Singular("Zur Startseite"), - 301 to Singular("Als eingelöst markiert am"), - 302 to Singular("Als nicht eingelöst markiert am"), - 303 to Singular("Als Einzelcodes anzeigen"), - 304 to Singular("Als Sammelcode anzeigen"), - 305 to Singular("%s von %s"), - 306 to Singular("Rezepte eingelöst?"), - 307 to Singular("Möchten Sie die Rezepte als eingelöst markieren?"), - 308 to Singular("Nicht eingelöst"), - 309 to Singular("Eingelöst"), - 310 to Singular("Öffnet um %s Uhr"), - 311 to Singular("+49 800 277 377 7"), - 312 to Singular("Technische Hotline"), - 313 to Singular("Scanner für Rezepte öffnen"), - 314 to Singular("Einstellungen"), - 315 to Singular("+49 800 277 377 7"), - 316 to Singular("Bitte identifizieren Sie sich via Fingerabdruck oder Gesichtserkennung."), - 317 to Singular("Hinweis"), - 318 to Singular("Diese Änderung wird erst nach einem Neustart der App wirksam."), - 319 to Singular("Okay"), - 320 to Singular("Tracking"), - 321 to - Singular("Helfen Sie uns, diese App besser zu machen. Alle Nutzerdaten werden anonym erhoben und dienen ausschließlich der Verbesserung des Nutzungserlebnisses."), - 322 to Singular("Tracking erlauben"), - 323 to - Singular("Im Falle eines Absturzes oder eines Fehlers der App sendet uns die App Hinweise zu den Gründen. Zudem werden Betriebssystemversion und Angaben zur verwendeten Hardware gesendet."), - 324 to Singular("Screenshots unterdrücken"), - 325 to Singular("Verhindert die Anzeige eines Vorschaubilds beim App-Wechsel"), - 326 to Singular("Erlauben Sie E-Rezept Ihr Nutzerverhalten anonym zu analysieren?"), - 327 to - Singular("Das umfasst Hard- und Softwareinformationen Ihres Telefons, Einstellungen der E-Rezept App sowie Umfang der Nutzung, jedoch niemals Daten über Ihre Person oder Ihre Gesundheit.\nDie Daten werden durch Datenverarbeitungsnehmer nur der gematik GmbH zur Verfügung gestellt und nach 180 Tagen spätestens gelöscht. Sie können die Analyse jederzeit wieder im Menü der App deaktivieren.\nWir können durch diese Daten nachvollziehen, welche Funktionen häufig genutzt werden, und diese verbessern. Ferner können wir einschätzen, wie lange ältere Technik unterstützt werden muss, und wann wir z. B. eine neuere Betriebssystemversion verpflichtend machen können, ohne (zu viele) Nutzer zu betreffen."), - 328 to Singular("Erlauben"), - 329 to Singular("Erlauben"), - 330 to Singular("Kennwort"), - 331 to Singular("Sichern Sie Ihre Daten mit einem selbstgewählten Passwort."), - 332 to Singular("Kennwort"), - 333 to Singular("Speichern"), - 334 to Singular("Kennwort anzeigen lassen"), - 335 to Singular("Kennwort eingeben"), - 336 to Singular("Sie können beliebige Zahlen, Buchstaben oder Sonderzeichen verwenden."), - 337 to Singular("Kennwort wiederholen"), - 338 to Singular("Kennwortstärke"), - 339 to Singular("Empfehlungen: %s"), - 340 to Plurals( - mapOf( - Plurals.Type.One to "Ihnen wurde %s Medikament verschrieben", - Plurals.Type.Other to "Ihnen wurden %s Medikamente verschrieben" - ) - ), - 341 to Singular("Hier tippen, um sie in einer Apotheke einzulösen"), - 342 to Singular("Jetzt einlösen"), - 343 to Singular("Alles anzeigen"), - 344 to Singular("Verordnung löschen"), - 345 to Singular("Als eingelöst markiert"), - 346 to Singular("Rückgängig"), - 347 to Singular("Mehr anzeigen"), - 348 to Singular("Weniger anzeigen"), - 349 to Singular("Technische Informationen"), - 350 to Singular("Abmelden"), - 351 to - Singular("Es werden alle Zugangsdaten zum Gesundheitsnetzwerk gelöscht. Ihre Rezeptdaten bleiben erhalten."), - 352 to Singular("Damit werden Ihre Zugangsdaten gelöscht."), - 353 to Singular("Abmelden"), - 354 to Singular("Abbrechen"), - 355 to Singular("Möchten Sie sich aus der App abmelden?"), - 356 to Singular("Sicherheit Ihrer Rezeptdaten"), - 357 to - Singular("Bitte achten Sie darauf, dass Personen, mit denen Sie gegebenenfalls dieses Gerät teilen und deren biometrische Merkmale auf diesem Gerät gespeichert sein könnten oder die über Geräte-PIN, Wischmuster oder Passwort verfügen, ebenfalls Zugriff auf Ihre Rezepte erhalten."), - 358 to Singular("Verbindlich einlösen?"), - 359 to - Singular("Hiermit werden Ihre Rezepte an diese Apotheke gesendet. Sie können sie anschließend in keiner anderen Apotheke mehr einlösen."), - 360 to Singular("Abbrechen"), - 361 to Singular("Jetzt einlösen"), - 362 to Singular("Erfolgreich eingelöst"), - 363 to - Singular("Die Apotheke wird sich schnellstmöglich mit Ihnen in Verbindung setzen, um Einzelheiten zur Lieferung mit Ihnen zu klären."), - 364 to Singular("Schließen Sie Ihre Bestellung im Browser ab"), - 365 to Singular("Wechseln Sie auf die Startseite"), - 366 to - Singular("Die Versandapotheke erstellt Ihnen einen Warenkorb mit Ihren Medikamenten. Dieser Vorgang kann einige Minuten dauern."), - 367 to - Singular("Tippen Sie auf „Warenkorb öffnen“ und schließen Sie Ihre Bestellung auf der Webseite der Apotheke ab."), - 368 to Singular("Zur Startseite"), - 369 to Singular("Senden fehlgeschlagen"), - 370 to Singular("Wiederholen"), - 371 to - Singular("Ihre Bestellung liegt üblicherweise zeitnah für Sie bereit. Für einen genauen Termin kontaktieren Sie bitte die Apotheke."), - 372 to Singular("Ihr Warenkorb steht bereit"), - 373 to Singular("Abholcode erhalten"), - 374 to Singular("Mitteilung erhalten"), - 375 to Singular("Abholcode anzeigen"), - 376 to Singular("Warenkorb öffnen"), - 377 to Singular("Zeigen Sie diesen Code in Ihrer Apotheke vor."), - 378 to Singular("Abholcode"), - 379 to Singular("Keine Mitteilungen"), - 380 to Singular("Sie haben noch keine Mitteilungen erhalten"), - 381 to - Singular("Leider war die Nachricht Ihrer Apotheke leer. Bitte kontaktieren Sie Ihre Apotheke."), - 382 to Singular("Kein E-Mail-Programm eingerichtet"), - 383 to Singular("Keine Ergebnisse"), - 384 to Singular("Unter diesem Suchbegriff konnten wir keine Ergebnisse finden."), - 385 to Singular("Keine Verbindung zum Server"), - 386 to Singular("Bitte probieren Sie es in einigen Minuten erneut"), - 387 to Singular("Erneut laden"), - 388 to Singular("Open Source Lizenzen"), - 389 to Singular("Kontakt"), - 390 to Singular("Technische Hotline anrufen"), - 391 to Singular("Mail schreiben"), - 392 to Singular("An Umfrage teilnehmen"), - 393 to Singular("Mail schreiben"), - 394 to Singular("+49 800 277 377 7"), - 395 to Singular("app-feedback@gematik.de"), - 396 to Singular("https://gematik.shortcm.li/app_feedback"), - 397 to Singular("Tokens anzeigen"), - 398 to Singular("Mehr erfahren"), - 399 to Singular("Lächelnde Familie"), - 400 to Singular("Apotheker hält ein Smartphone in der Hand und freut sich auf Sie."), - 401 to - Singular("Hand hält ein Smartphone in der Hand und authentifiziert sich mit der neuen elektronischen Gesundheitskarte in der App"), - 402 to Singular("Helfen Sie uns, diese App besser zu machen"), - 403 to Singular("Wir wollen:"), - 404 to Singular("Nutzerströme in der App %s analysieren, um die Benutzbarkeit zu verbessern."), - 405 to Singular("Abstürze und Fehlermeldungen %s an die Entwickler senden."), - 406 to - Singular("Fehlermuster frühzeitig erkennen, für eine Verbesserung der technischen Hotline."), - 407 to Singular("Ich möchte dabei helfen, diese App besser zu machen"), - 408 to Singular("Sie können diese Entscheidung in den Systemeinstellungen jederzeit ändern."), - 409 to Singular("anonym"), - 410 to Singular("Weiter"), - 411 to Singular("Wie möchten Sie diese App absichern?"), - 412 to - Singular("Machen Sie es Unbefugten schwerer, an Ihre Daten zu gelangen und sichern Sie den Start der App."), - 413 to Singular("Oder"), - 414 to Singular("Beste Gerätesicherung wählen"), - 415 to Singular("Beste Gerätesicherung gewählt"), - 416 to - Singular("Diese App verwendet die sicherste Methode, die von Ihrem Gerät zur Verfügung gestellt wird. Hierbei kann es sich um Fingerabdruck, Wischmuster oder ähnliches handeln."), - 417 to - Singular("Das umfasst Hard- und Softwareinformationen Ihres Telefons, Einstellungen der E-Rezept App sowie Umfang der Nutzung, jedoch niemals Daten über Ihre Person oder Ihre Gesundheit."), - 418 to - Singular("Die Daten werden durch Datenverarbeitungsnehmer nur der gematik GmbH zur Verfügung gestellt und nach 180 Tagen spätestens gelöscht. Sie können die Analyse jederzeit wieder im Menü der App deaktivieren."), - 419 to - Singular("Wir können durch diese Daten nachvollziehen, welche Funktionen häufig genutzt werden, und diese verbessern. Ferner können wir einschätzen, wie lange ältere Technik unterstützt werden muss, und wann wir z.B. eine neuere Betriebssystemversion verpflichtend machen können, ohne (zu viele) Nutzer zu betreffen."), - 420 to Singular("App verbessern"), - 421 to Singular("Ablehnen"), - 422 to Singular("Anonyme Analyse bleibt deaktiviert"), - 423 to Singular("%s Vielen Dank für Ihre Unterstützung!"), - 424 to Singular("\u2661"), - 425 to Singular("Wir freuen uns auf Ihr Feedback"), - 426 to Singular("Je konkreter, desto besser"), - 427 to - Singular("Beim Senden Ihrer Nachricht werden folgende Informationen über genutzte Hardware und Betriebssystem übertragen:"), - 428 to Singular("Betriebssystem"), - 429 to Singular("Android %s (Entwicklerversion %s) (letztes Sicherheitsupdate %s)"), - 430 to Singular("Modell"), - 431 to Singular("%s %s (Codename %s)"), - 432 to Singular("Modus"), - 433 to Singular("Dunkles Design"), - 434 to Singular("Helles Design"), - 435 to Singular("Sprache"), - 436 to Singular("Senden"), - 437 to Singular("Feedback"), - 438 to Singular("Bestellen oder reservieren"), - 439 to Singular("Rezepte werden automatisch als eingelöst markiert"), - 440 to - Singular("Es kann zu einer Verzögerung kommen, bis eingelöste Rezepte im Bereich „Archiv“ angezeigt werden."), - 441 to Singular("Okay"), - 442 to Singular("Sie müssen angemeldet sein, um Rezepte zu löschen."), - 443 to Singular("Fehler melden"), - 444 to Singular("Fehlerhafte Mitteilung erhalten"), - 445 to Singular("Eine Apotheke hat eine Mitteilung in einem fehlerhaften Format versendet."), - 446 to Singular("app-fehlermeldung@ti-support.de"), - 447 to Singular("Fehlermeldung aus der E-Rezept App"), - 448 to - Singular("Liebes Service-Team, ich habe eine Nachricht von einer Apotheke erhalten. Leider konnte ich meinem Nutzer die Nachricht aber nicht mitteilen, da ich sie nicht verstanden habe. Bitte prüft, was hier passiert ist, und helft uns. Vielen Dank! Die E-Rezept App"), - 449 to - Singular("Die folgenden Informationen würde ich gerne dem Service-Team mitteilen, damit die Fehlersuche durchgeführt werden kann. Bitte beachten Sie, dass wir auch Ihre eMail-Adresse sowie ggf. Ihren Namen erfahren, wenn Sie ihn als Absender der eMail konfiguriert haben. Wenn Sie diese Informationen ganz oder teilweise nicht übermitteln möchten, löschen Sie diese bitte aus der Mail. Alle Daten werden von der gematik GmbH oder deren beauftragten Unternehmen nur zur Bearbeitung dieser Fehlermeldung gespeichert und verarbeitet. Die Löschung erfolgt automatisiert, spätestens 180 Tage nach Erledigung des Tickets. Ihre eMail-Adresse nutzen wir ausschließlich, um mit Ihnen Kontakt in Bezug auf diese Fehlermeldung aufzunehmen. Für Fragen oder eine vorzeitige Löschung können Sie sich jederzeit an den Datenschutzverantwortlichen des E-Rezept Systems wenden. Sie finden weitere Informationen in der E-Rezept App im Menü unter dem Datenschutz-Eintrag."), - 450 to Singular("Fehler 40 42 67336"), - 451 to Singular("Hinweis"), - 452 to Singular("Für dieses Gerät wurde keine Gerätesicherung eingerichtet"), - 453 to - Singular("Wir empfehlen Ihnen, Ihre medizinischen Daten zusätzlich durch eine Gerätesicherung wie beispielsweise einen Code oder Biometrie zu schützen."), - 454 to Singular("Diesen Hinweis in Zukunft nicht mehr anzeigen."), - 455 to Singular("Anmelden"), - 456 to Singular("Bitte identifizieren Sie sich um Rezepte herunterzuladen."), - 457 to Singular("Eingelöst am %s"), - 458 to Singular("Sie haben ein Ersatzpräparat erhalten"), - 459 to - Singular("Bitte beachten Sie die Einnahmehinweise in Ihrem Medikationsplan oder die schriftliche Dosierungsanweisung Ihres Arztes."), - 460 to - Singular("Hinweis für die Apotheken: Die Kontaktdaten und Informationen zu Apotheken beziehen wir von mein-apothekenportal.de des Deutschen Apothekenverbandes e.V. Sie haben einen Fehler entdeckt oder möchten Daten korrigieren?"), - 461 to Singular("mein-apothekenportal.de"), - 462 to Singular("Mehr erfahren"), - 463 to Singular("https://www.mein-apothekenportal.de/"), - 464 to Singular("https://www.gematik.de/anwendungen/e-rezept/faq/meine_apotheke/"), - 465 to Singular("Einlösen bald möglich"), - 466 to Singular("Diese Apotheke kann derzeit noch keine E-Rezepte in Empfang nehmen."), - 467 to Singular("E-Rezept"), - 468 to Singular("Bereit für das E-Rezept"), - 469 to Singular("Aktuell geöffnet"), - 470 to Singular("Botendienst"), - 471 to Singular("Versand"), - 472 to Singular("Filter"), - 473 to Singular("Beliebte Filter"), - 474 to Singular("Filtern"), - 475 to Singular("Möglicherweise ist die Standortfreigabe in den Einstellungen deaktiviert."), - 476 to Singular("Kein Standort verfügbar"), - 477 to - Singular("Verbindung fehlgeschlagen. Eine Netzwerkverbindung konnte nicht aufgebaut werden."), - 478 to Singular("Kommunikation mit dem Server fehlgeschlagen: Statuscode %s."), - 479 to Singular("Kommunikation mit dem Server fehlgeschlagen: VAU Fehler"), - 480 to Singular("Keine aktiven Tokens"), - 481 to Singular("Als eingelöst markiert"), - 482 to Singular("Als nicht eingelöst markiert"), - 483 to Singular("Warnung"), - 484 to Singular("Diesem Gerät darf eventuell nicht voll vertraut werden"), - 485 to - Singular("Diese App sollte aus Sicherheitsgründen nicht auf gerooteten Geräten genutzt werden."), - 486 to Singular("Ich nehme das erhöhte Risiko zur Kenntnis und möchte dennoch fortfahren."), - 487 to Singular("Weshalb sind Geräte mit Root-Zugriff ein potentielles Sicherheitsrisiko?"), - 488 to Singular("Mehr erfahren"), - 489 to Singular("https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html"), - 490 to Singular("Hinweis"), - 491 to Singular("Neue Gesundheitskarte beantragen"), - 492 to Singular("Um sich erfolgreich in dieser App anmelden zu können, benötigen Sie"), - 493 to Singular(" "), - 494 to Singular("Eine %s mit Zugangsnummer (CAN)"), - 495 to Singular("Gesundheitskarte"), - 496 to Singular("die zugehörige %s."), - 497 to Singular("Pin"), - 498 to - Singular("Bitten Sie ausdrücklich um die Zusendung von Karte & PIN zum Zwecke der Nutzung der E-Rezept-App."), - 499 to - Singular("Die Gesundheitskarte und die zugehörige PIN erhalten Sie kostenfrei von Ihrer Krankenversicherung. Der Antrag kann formlos und per %s gestellt werden."), - 500 to Singular("Mail"), - 501 to Singular("Mit Gesundheitskarte anmelden"), - 502 to Singular("In Kürze: Mit Kassen-App anmelden"), - 503 to Singular("Sichere Anmeldung mit Ihrer neuen elektronischen Gesundheitskarte"), - 504 to Singular("Nutzen Sie eine App Ihrer Krankenversicherung zur Freischaltung"), - 505 to Singular("Wie möchten Sie sich anmelden?"), - 506 to - Singular("Um automatisch Rezepte zu empfangen und leicht Medikamente online einlösen oder reservieren zu können, müssen Sie sich anmelden."), - 507 to Singular("Anmeldung"), - 508 to Singular("Mann meldet sich mit Gesundheitskarte an"), - 509 to Singular("Frau meldet sich mit Kassen-App an"), - 510 to Singular("Leider hat ihr Gerät kein NFC um diese Funktion zu nutzen."), - 511 to Singular("Name des Profils"), - 512 to Singular("Bitte geben Sie einen Namen für das neue Profil ein."), - 513 to Singular("Profilname"), - 514 to Singular("Profilnamen ändern"), - 515 to Singular("Löschen"), - 516 to Singular("Profile"), - 517 to Singular("Wie sollen wir Sie nennen?"), - 518 to - Singular("Das hilft Ihnen dabei, den Überblick zu behalten, wenn Sie die Rezepte für mehrere Personen verwalten möchten."), - 519 to Singular("Name eingeben"), - 520 to Singular("Profile verwalten"), - 521 to - Singular("Legen Sie Profile für Ihre Familie oder Angehörige an. Melden Sie sich mit der Gesundheitskarte an, um online bestellen zu können."), - 522 to Singular("Profil hinzufügen"), - 523 to Singular("Speichern"), - 524 to Singular("Gesundheitskarte"), - 525 to Singular("Krankenversicherung kontaktieren"), - 526 to - Singular("Um sich in dieser App anmelden zu können, benötigen Sie eine NFC-fähige Gesundheitskarte sowie eine zugehörige PIN."), - 527 to - Singular("Diese erhalten Sie kostenfrei von Ihrer Krankenversicherung. Hierfür müssen Sie sich mittels amtlichem Ausweisdokument identifiziert haben."), - 528 to Singular("So erkennen Sie eine NFC-fähige Gesundheitskarte"), - 529 to - Singular("https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204"), - 530 to Singular("Krankenversicherung wählen"), - 531 to Singular("Keine Auswahl"), - 532 to Singular("Was möchten Sie beantragen?"), - 533 to Singular("Keine Kontaktaufnahme über diese App möglich"), - 534 to Singular("Bitte nutzen Sie die üblichen Kanäle, um Ihre Versicherung zu kontaktieren."), - 535 to Singular("Gesundheitskarte & PIN"), - 536 to Singular("Nur PIN"), - 537 to Singular("Kontaktieren Sie Ihre Krankenversicherung"), - 538 to Singular("Anmeldung in der E-Rezept App"), - 539 to Singular("Neue Gesundheitskarte bestellen"), - 540 to - Singular("Für die Anmeldung benötigen Sie eine geeignete Karte mit NFC. Wir unterstützen Sie bei der Bestellung."), - 541 to Singular("Fortfahren"), - 542 to Singular("Mit Karte anmelden"), - 543 to Singular("Keine Verbindung möglich"), - 544 to - Singular("Leider erfüllt Ihr Smartphone die Mindestanforderungen für die Nutzung der E-Rezept-App mit Ihrer elektronischen Gesundheitskarte nicht."), - 545 to - Singular("Warum gibt es Mindestanforderungen für die Verbindung von App und elektronischer Gesundheitskarte?"), - 546 to Singular("Das Namensfeld darf nicht leer sein."), - 547 to Singular("Ein Profil mit dem eingegebenen Namen existiert bereits."), - 548 to Singular("Profil"), - 549 to Singular("%s ausgewählt"), - 550 to Singular("Hintergrundfarbe"), - 551 to Singular("Frühlingsgrau"), - 552 to Singular("Sonnentau"), - 553 to Singular("Es! Ist! Rosa!"), - 554 to Singular("Baum"), - 555 to Singular("Blauer Mond September"), - 556 to Singular("Nicht angemeldet"), - 557 to Singular("Verbunden"), - 558 to Singular("Zuletzt verbunden am %s"), - 559 to Singular("Profil löschen?"), - 560 to - Singular("Hiermit werden alle Daten des Profils auf diesem Gerät gelöscht. Ihre Rezepte im Gesundheitsnetzwerk bleiben erhalten."), - 561 to Singular("Löschen"), - 562 to Singular("Abbrechen"), - 563 to Singular("Profil löschen"), - 564 to Singular("Sie möchten das letzte Profil löschen."), - 565 to - Singular("Die App benötigt mindestens ein Profil. Bitte geben Sie einen Namen für das neue Profil ein."), - 566 to Singular("Fehler 20 10 76831"), - 567 to - Singular("Das Verzeichnis der Gesundheitskarten konnte nicht erreicht werden. Bitte versuchen Sie es erneut."), - 568 to - Singular("Hiermit stellen Sie eine Verbindung zum Gesundheitsnetzwerk her. Sie erhalten dadurch automatisch neue Rezepte oder Nachrichten."), - 569 to - Singular("Fachlich geprüfte Informationen zu Krankheiten, ICD-Codes und zu Vorsorge- und Pflegethemen finden Sie im Nationalen Gesundheitsportal."), - 570 to Singular("gesund.bund.de öffnen"), - 571 to Singular("https://gesund.bund.de/"), - 572 to Singular("Profil wählen"), - 573 to Singular("Profile bearbeiten"), - 574 to Singular("Keine neuen Rezepte verfügbar"), - 575 to Plurals( - mapOf( - Plurals.Type.One to "%s Rezept aktualisiert", - Plurals.Type.Other to - "%s Rezepte aktualisiert" - ) - ), - 576 to Singular("Einlösbar"), - 577 to Singular("Rezept ist übertragen"), - 578 to Singular("Eingelöst"), - 579 to Singular("Unbekannt"), - 580 to Singular("Details"), - 581 to Singular("Zugriffsprotokolle anzeigen"), - 582 to Singular("Hier können Sie sehen, wer auf Ihre Rezepte zugegriffen hat"), - 583 to Singular("Hierbei handelt es sich um Zugangsschlüssel zum Rezeptdienst"), - 584 to Singular("Zugriffsprotokolle"), - 585 to Singular("Ausloggen"), - 586 to Singular("Einloggen"), - 587 to Singular("Die Funktion steht im Demomodus nicht zur Verfügung"), - 588 to Singular("Das Rezept wurde übertragen."), - 589 to - Singular("Das Rezept wird von Ihrem Arzt / Ihrer Ärztin direkt an die Apotheke weitergeleitet."), - 590 to Singular("Keine Zugriffsprotokolle"), - 591 to Singular("Sie erhalten Zugriffsprotokolle, wenn Sie am Rezeptdienst angemeldet sind."), - 592 to Singular("Es liegen noch keine Zugriffsprotokolle vor."), - 593 to Singular("Zuletzt aktualisiert am %s"), - 594 to Singular("Das Rezept ist derzeit in Bearbeitung und kann nicht gelöscht werden"), - 595 to - Singular("Dieses Profil wurde noch nicht mit einer Versichertennummer verbunden. Hierfür müssen Sie sich am Rezeptserver anmelden."), - 596 to Singular("Verknüpft mit:"), - 597 to Singular("Am Rezeptserver anmelden?"), - 598 to Singular("Komfortabel neue Rezepte empfangen und einlösen."), - 599 to Singular("Anmelden"), - 600 to Singular("Im Demomodus nicht verfügbar."), - 601 to Singular("Diese Funktion wird in einem kommenden Update freigeschaltet."), - 602 to Singular("Info"), - 603 to Singular("Nutzungsbedingungen"), - 604 to Singular("Datenschutz"), - 605 to Singular("E-Rezept"), - 606 to Singular("Die App für Desktop"), - 607 to Singular("Mit Gesundheitskarte anmelden"), - 608 to Singular("Rezepte"), - 609 to Singular("Apotheken"), - 610 to Singular("Einlösbar"), - 611 to Singular("Bereits eingelöst"), - 612 to Singular("Benachrichtigungen"), - 613 to Singular("Protokoll"), - 614 to Singular("Aktualisieren"), - 615 to Singular("Vergrößern"), - 616 to Singular("Verkleinern"), - 617 to Singular("Hilfe"), - 618 to Singular("Beenden"), - 619 to Singular("Keine Angabe"), - 620 to Singular("Nutzungsbedingungen & Datenschutz"), - 621 to Singular("Kartenlesegerät anschließen"), - 622 to Singular("Kartenzugangsnummer eingeben"), - 623 to Singular("PIN eingeben"), - 624 to Singular("Mit Gesundheitskarte anmelden"), - 625 to Singular("Suche nach Kartenlesegerät"), - 626 to Singular("Kartenlesegerät gefunden"), - 627 to - Singular("Treiber des Kartenlesegeräts möglicherweise nicht geladen. Bitte verbinden Sie das Kartenlesegerät vor dem Start der E-Rezept-Anwendung."), - 628 to Singular("Lesen der Gesundheitskarte fehlgeschlagen."), - 629 to Singular("Legen Sie die Gesundheitskarte erneut auf das Lesegerät."), - 630 to Plurals( - mapOf( - Plurals.Type.Zero to "Nur noch heute als Selbstzahlender einlösbar", - Plurals.Type.One to "Noch %s Tag als Selbstzahlender einlösbar", - Plurals.Type.Other to - "Noch %s Tage als Selbstzahlender einlösbar" - ) - ), - 631 to Plurals( - mapOf( - Plurals.Type.Zero to "Nur noch heute gültig", - Plurals.Type.One to - "Noch %s Tag gültig", - Plurals.Type.Other to "Noch %s Tage gültig" - ) - ), - 632 to Singular("Nicht mehr gültig"), - 633 to Singular("Ausgestellt am %s"), - 634 to Singular("Rezeptcode anzeigen"), - 635 to Singular("Abholung"), - 636 to Singular("Botendienst"), - 637 to Singular("Versand"), - 638 to Singular("https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz"), - 639 to Singular("https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz"), - 640 to Singular("https://www.das-e-rezept-fuer-deutschland.de/faq"), - 641 to Singular("https://www.das-e-rezept-fuer-deutschland.de/kontakt"), - 642 to Singular("Mitglied"), - 643 to Singular("Familienangehörig"), - 644 to Singular("Rentner*in"), - 645 to Singular("Keine Angabe"), - 646 to Singular("Keine therapiegerechte Packungsgröße"), - 647 to Singular("Normgröße 1"), - 648 to Singular("Normgröße 2"), - 649 to Singular("Normgröße 3"), - 650 to Singular("Nicht betroffen"), - 651 to Singular("Sonstiges"), - 652 to Singular("Ätherisches Öl"), - 653 to Singular("Ampullen"), - 654 to Singular("Ampullenpaare"), - 655 to Singular("Augen- und Nasensalbe"), - 656 to Singular("Augen- und Ohrensalbe"), - 657 to Singular("Augen- und Ohrentropfen"), - 658 to Singular("Augentropfen"), - 659 to Singular("Augenbad"), - 660 to Singular("Augencreme"), - 661 to Singular("Augengel"), - 662 to Singular("Augensalbe"), - 663 to Singular("Bad"), - 664 to Singular("Balsam"), - 665 to Singular("Bandage"), - 666 to Singular("Beutel"), - 667 to Singular("Binden"), - 668 to Singular("Bonbons"), - 669 to Singular("Basisplatte"), - 670 to Singular("Brei"), - 671 to Singular("Brausetabletten"), - 672 to Singular("Creme"), - 673 to Singular("Durchstechflaschen"), - 674 to Singular("Dilution"), - 675 to Singular("Depot-Injektionssuspension"), - 676 to Singular("Dragées in Kalenderpackung"), - 677 to Singular("Dosieraerosol"), - 678 to Singular("Dragées"), - 679 to Singular("Magensaftresistente Dragées"), - 680 to Singular("Dosierschaum"), - 681 to Singular("Dosierspray"), - 682 to Singular("Einzeldosis-Pipette"), - 683 to Singular("Einreibung"), - 684 to Singular("Elektroden"), - 685 to Singular("Elixier"), - 686 to Singular("Emulsion"), - 687 to Singular("Essenz"), - 688 to Singular("Erwachsenen-Suppositorien"), - 689 to Singular("Extrakt"), - 690 to Singular("Filterbeutel"), - 691 to Singular("Franzbranntwein"), - 692 to Singular("Filmdragées"), - 693 to Singular("Fertigspritze"), - 694 to Singular("Fettsalbe"), - 695 to Singular("Flasche"), - 696 to Singular("Flüssigkeit zum Einnehmen"), - 697 to Singular("Flüssig"), - 698 to Singular("Magensaftresistente Filmtabletten"), - 699 to Singular("Folie"), - 700 to Singular("Beutel mit retardierten Filmtabletten"), - 701 to Singular("Flüssigseife"), - 702 to Singular("Filmtabletten"), - 703 to Singular("Granulat zur Entnahme aus Kapseln"), - 704 to Singular("Gel"), - 705 to Singular("Gas und Lösungsmittel zur Herstellung einer Injektions-/Infusionsdispersion"), - 706 to Singular("Globuli"), - 707 to Singular("Magensaftresistentes Granulat"), - 708 to Singular("Gelplatte"), - 709 to Singular("Garnulat"), - 710 to Singular("Granulat zur Herstellung einer Suspension zum Einnehmen"), - 711 to Singular("Gurgellösung"), - 712 to Singular("Handschuhe"), - 713 to Singular("Magensaftresistente Hartkapseln"), - 714 to Singular("Hartkapseln"), - 715 to Singular("Hartkapseln mit Pulver zur Inhalation"), - 716 to Singular("Hartkapseln mit veränderter Wirkstofffreisetzung"), - 717 to Singular("Infusionsampullen"), - 718 to Singular("Infusionsbeutel"), - 719 to Singular("Infusionsdispersion"), - 720 to Singular("Injektionslösung in einer Fertigspritze"), - 721 to Singular("Infusionsflaschen"), - 722 to Singular("Infusionslösungskonzentrat"), - 723 to Singular("Injektions-Flaschen"), - 724 to Singular("Infusionsset"), - 725 to Singular("Inhalations-Ampullen"), - 726 to Singular("Inhalations-Pulver"), - 727 to Singular("Injektions- oder Infusionslösung oder Lösung zum Einnehmen"), - 728 to Singular("Injektions- Infusions-Lösung"), - 729 to Singular("Injektionslösung zur intramuskulären Anwendung"), - 730 to Singular("Inhalations-Kapseln"), - 731 to Singular("Injektions-Lösung"), - 732 to Singular("Implantat"), - 733 to Singular("Infusionslösung"), - 734 to Singular("Inhalat"), - 735 to Singular("Injektions- Infusions-Flaschen"), - 736 to Singular("Inhalations-Lösung"), - 737 to Singular("Instant-Tee"), - 738 to Singular("Instillation"), - 739 to Singular("Injektions-Suspension"), - 740 to Singular("Intrauterinpessar"), - 741 to Singular("Kanülen"), - 742 to Singular("Kapseln"), - 743 to Singular("Katheter"), - 744 to Singular("Kaudragées"), - 745 to Singular("Kegel"), - 746 to Singular("Kerne"), - 747 to Singular("Kaugummi"), - 748 to Singular("Konzentrat zur Herstellung einer Infusionsdispersion"), - 749 to Singular("Konzentrat zur Herstellung einer Injektions- oder Infusionslösung"), - 750 to Singular("Kleinkinder-Suppositorien"), - 751 to Singular("Klistiere"), - 752 to Singular("Klistier-Tabletten"), - 753 to Singular("Hartkapseln mit Magensaft-resistent überzogenen Pellets"), - 754 to Singular("Magensaftresistente Kapseln"), - 755 to Singular("Kondome"), - 756 to Singular("Kompressen"), - 757 to Singular("Konzentrat"), - 758 to Singular("Kombipackung"), - 759 to Singular("Kristallsuspension"), - 760 to Singular("Kinder- und Säuglings-Suppositorien"), - 761 to Singular("Kinder-Suppositorien"), - 762 to Singular("Kautabletten"), - 763 to Singular("Lanzetten"), - 764 to Singular("Lösung zur Injektion, Infusion oder Inhalation"), - 765 to Singular("Liquidum"), - 766 to Singular("Lösung"), - 767 to Singular("Lotion"), - 768 to Singular("Lösung für einen Vernebler"), - 769 to Singular("Lösung zum Einnehmen"), - 770 to Singular("Lacktabletten"), - 771 to Singular("Lutschpastillen"), - 772 to Singular("Lutschtabletten"), - 773 to Singular("Milch"), - 774 to Singular("Mischung"), - 775 to Singular("Mixtur"), - 776 to Singular("Magensaftresistentes Retardgranulat"), - 777 to Singular("Magensaftresistente Pellets"), - 778 to Singular("Manteltabletten"), - 779 to Singular("Mundwasser"), - 780 to Singular("Nasengel"), - 781 to Singular("Nasenöl"), - 782 to Singular("Nasenspray"), - 783 to Singular("Wirkstoffhaltiger Nagellack"), - 784 to Singular("Nasendosierspray"), - 785 to Singular("Nasensalbe"), - 786 to Singular("Nasentropfen"), - 787 to Singular("Occusert"), - 788 to Singular("Öl"), - 789 to Singular("Ohrentropfen"), - 790 to Singular("Ovula"), - 791 to Singular("Packungsmasse"), - 792 to Singular("Pastillen"), - 793 to Singular("Pellets"), - 794 to Singular("Injektionslösung in einem Fertigpen"), - 795 to Singular("Perlen"), - 796 to Singular("Pflaster"), - 797 to Singular("Pflaster-transdermal"), - 798 to Singular("Pulver zur Herstellung einer Injektions- Infusions- oder Inhalationslösung"), - 799 to - Singular("Pulver zur Herstellung einer Injektions- bzw. Infusionslösung oder Pulver und Lösungsmittel zur Herstellung einer Lösung zur intravesikalen Anwendung"), - 800 to - Singular("Pulver für ein Konzentrat zur Herstellung einer Infusionslösung Pulver zur Herstellung einer Lösung zum Einnehmen"), - 801 to Singular("Pulver zur Herstellung einer Infusionslösung"), - 802 to Singular("Pulver zur Herstellung einer Injektions- oder Infusionslösung"), - 803 to Singular("Pulver zur Herstellung einer Injektionslösung"), - 804 to Singular("Pulver zur Herstellung eines Infusionslösungskonzentrats"), - 805 to Singular("Pulver zur Herstellung einer Infusionssuspension"), - 806 to - Singular("Pulver zur Herstellung einer Injektions- bzw. Infusionslösung oder einer Lösung zur intravesikalen Anwendung"), - 807 to Singular("Pulver für ein Konzentrat zur Herstellung einer Infusionslösung"), - 808 to Singular("Pulver zur Herstellung einer Lösung zum Einnehmen"), - 809 to Singular("Pulver und Lösungsmittel zur Herstellung einer Infusionslösung"), - 810 to Singular("Perlongetten"), - 811 to - Singular("Pulver und Lösungsmittel zur Herstellung einer Injektions- bzw. Infusionslösung"), - 812 to Singular("Pulver und Lösungsmittel zur Herstellung einer Injektionslösung"), - 813 to - Singular("Pulver und Lösungsmittel für ein Konzentrat zur Herstellung einer Infusionslösung"), - 814 to Singular("Pulver und Lösungsmittel zur Herstellung einer Injektionssuspension"), - 815 to - Singular("Pulver und Lösungsmittel zur Herstellung einer Lösung zur intravesikalen Anwendung"), - 816 to Singular("Pumplösung"), - 817 to Singular("Presslinge"), - 818 to Singular("Pulver zur Herstellung einer Suspension zum Einnehmen"), - 819 to Singular("Paste"), - 820 to Singular("Puder"), - 821 to Singular("Pulver"), - 822 to Singular("Retard-Dragées"), - 823 to Singular("Retard-Kapseln"), - 824 to Singular("Retard-Tabletten"), - 825 to Singular("Retard-Granulat"), - 826 to Singular("Rektalkapseln"), - 827 to Singular("Retardmikrokapseln und Suspensionsmittel"), - 828 to Singular("Rektalschaum"), - 829 to Singular("Rektalsuspension"), - 830 to Singular("Retard-überzogene-Tabletten"), - 831 to Singular("Saft"), - 832 to Singular("Salbe"), - 833 to Singular("Salbe zur Anwendung in der Mundhöhle"), - 834 to Singular("Schaum"), - 835 to Singular("Seife"), - 836 to Singular("Shampoo"), - 837 to Singular("Sirup"), - 838 to Singular("Salz"), - 839 to Singular("Schmelzfilm"), - 840 to Singular("Schmelztabletten"), - 841 to Singular("Suppositorien mit Mulleinlage"), - 842 to Singular("Spritzampullen"), - 843 to Singular("Sprühflasche"), - 844 to Singular("Spüllösung"), - 845 to Singular("Spray"), - 846 to Singular("Transdermales Spray"), - 847 to Singular("Spritzen"), - 848 to Singular("Säuglings-Suppositorien"), - 849 to Singular("Stechampullen"), - 850 to Singular("Stäbchen"), - 851 to Singular("Stifte"), - 852 to Singular("Streifen"), - 853 to Singular("Substanz"), - 854 to Singular("Suspension zum Einnehmen"), - 855 to Singular("Sublingualspray Lösung"), - 856 to Singular("Suppositorien"), - 857 to Singular("Suspension"), - 858 to Singular("Sublingualtabletten"), - 859 to Singular("Suspension für Vernebler"), - 860 to Singular("Schwämme"), - 861 to Singular("Tabletten"), - 862 to Singular("Täfelchen"), - 863 to Singular("Trockenampullen"), - 864 to Singular("Tee"), - 865 to Singular("Tropfen zum Einnehmen"), - 866 to Singular("Test"), - 867 to Singular("Tinktur"), - 868 to Singular("Tabletten in Kalenderpackung"), - 869 to Singular("Tablette zur Herstellung einer Lösung zum Einnehmen"), - 870 to Singular("Magensaftresistente Tabletten"), - 871 to Singular("Tonikum"), - 872 to Singular("Tampon"), - 873 to Singular("Tamponaden"), - 874 to Singular("Trinkampullen"), - 875 to Singular("Trituration"), - 876 to Singular("Tropfen"), - 877 to Singular("Trockensubstanz mit Lösungsmittel"), - 878 to Singular("Trinktablette"), - 879 to Singular("Trockensaft"), - 880 to - Singular("Tabletten zur Herstellung einer Suspension zum Einnehmen für einen Dosierspender"), - 881 to Singular("Tablette zur Herstellung einer Suspension zum Einnehmen"), - 882 to Singular("Trockensubstanz ohne Lösungsmittel"), - 883 to Singular("Teststäbchen"), - 884 to Singular("Transdermales System"), - 885 to Singular("Teststreifen"), - 886 to Singular("Tube"), - 887 to Singular("Tücher"), - 888 to Singular("Tupfer"), - 889 to Singular("Tablette mit veränderter Wirkstofffreisetzung"), - 890 to Singular("Überzogene Tablette"), - 891 to Singular("Vaginallösung"), - 892 to Singular("Vaginalring"), - 893 to Singular("Vaginalcreme"), - 894 to Singular("Verband"), - 895 to Singular("Vaginalgel"), - 896 to Singular("Vaginalkapseln"), - 897 to Singular("Vlies"), - 898 to Singular("Vaginalovula"), - 899 to Singular("Vaginalstäbchen"), - 900 to Singular("Vaginalsuppositorien"), - 901 to Singular("Vaginaltabletten"), - 902 to Singular("Watte"), - 903 to Singular("Wundgaze"), - 904 to Singular("Weichkapseln"), - 905 to Singular("Magensaftresistente Hartkapseln"), - 906 to Singular("Würfel"), - 907 to Singular("Duschgel"), - 908 to Singular("Deo-Spray"), - 909 to Singular("Festiger"), - 910 to Singular("Gesichtsmaske"), - 911 to Singular("Halsband"), - 912 to Singular("Haarspülung"), - 913 to Singular("Nachtcreme"), - 914 to Singular("Körperpflege"), - 915 to Singular("Tagescreme"), - 916 to Singular("Zylinderampullen"), - 917 to Singular("Zahnbürste"), - 918 to Singular("Zahncreme"), - 919 to Singular("Zahngel"), - 920 to Singular("Zerbeißkapseln"), - 921 to Singular("Zahnpasta") - ) -) - -public val stringsEn: Strings = Strings( - mapOf( - 0 to Singular("E-prescription"), - 1 to Singular("OK"), - 3 to Singular("Cancel"), - 4 to Singular("Back"), - 5 to Singular("at"), - 6 to Singular("%1\$s o\'clock"), - 7 to Singular("Last updated on %1\$s"), - 8 to Singular("Update failed. Please update your prescriptions again."), - 9 to Singular("Digital. Fast. Secure."), - 11 to Singular("Welcome to the e-prescription app"), - 12 to - Singular("Here you can redeem electronic prescriptions at a pharmacy of your choice, directly in person or online."), - 13 to Singular("More features with your medical card"), - 14 to Singular("Automatically update your new prescriptions"), - 15 to Singular("Information on how to take your medication and dosages"), - 16 to Singular("Receive messages from your pharmacy about your order"), - 17 to Singular("Terms of Use & Privacy Policy"), - 18 to - Singular("In order to use the app, please agree to the Terms of Use and confirm that you have read and understood the Privacy Policy. Only data that is essential for the functioning of the services is collected."), - 19 to Singular("I have read and accept the %s."), - 20 to Singular("Terms of Use"), - 21 to Singular("Privacy Policy"), - 22 to Singular("Confirm"), - 23 to Singular("Next"), - 24 to Singular("Confirm"), - 25 to Singular("Add prescriptions"), - 26 to - Singular("Have you received a prescription printout? You add prescriptions to the app by scanning the respective prescription code."), - 27 to Singular("Agreed"), - 28 to Singular("Task ID"), - 29 to Singular("Access code"), - 30 to Singular("Copied"), - 32 to Singular("Terms of Use"), - 33 to Singular("Privacy Policy"), - 34 to Singular("Accept Terms of Use"), - 35 to Singular("Accept Privacy Policy"), - 36 to Singular("Prescriptions"), - 37 to Singular("Prescriptions"), - 38 to Singular("Messages"), - 40 to Singular("Redeem"), - 43 to Singular("e.g. dermatologist"), - 44 to Singular("%s %s detected from %s %s. Scan more codes?"), - 45 to Plurals(mapOf(Plurals.Type.One to "Prescription", Plurals.Type.Other to "Prescriptions")), - 46 to Plurals(mapOf(Plurals.Type.One to "Prescription", Plurals.Type.Other to "Prescriptions")), - 47 to Plurals( - mapOf( - Plurals.Type.One to "Add %s prescription", - Plurals.Type.Other to - "Add %s prescriptions" - ) - ), - 48 to Singular("Access to camera denied"), - 49 to - Singular("To use the scanner, you must allow the app to access your camera in the system settings."), - 50 to Singular("Focus the camera on a prescription code"), - 51 to Singular("This is not a valid prescription code"), - 52 to Singular("This prescription code has already been scanned"), - 53 to Plurals( - mapOf( - Plurals.Type.One to "%s prescription recognised", - Plurals.Type.Other to - "%s prescriptions recognised" - ) - ), - 54 to Singular("Cancel"), - 55 to Singular("Camera light"), - 56 to Singular("Cancel scanning of prescription codes?"), - 57 to Singular("Cancel scanning"), - 58 to Singular("Continue"), - 63 to Singular("Add card"), - 64 to Singular("Let\'s get started"), - 65 to Singular("Use all functions now"), - 66 to - Singular("To be able to use all functions of the app, log in with your medical card. You will receive this card and the required login details from your health insurance company."), - 67 to Singular("What you need:"), - 69 to Singular("A medical card with access number (CAN)"), - 70 to Singular("The PIN for the medical card"), - 71 to - Singular("Your card access number (CAN) has six digits. You will find the CAN in the top right-hand corner of the front of your medical card. If there is no six-digit access number here, you will need a new medical card from your health insurance company."), - 72 to Singular("Enter access number"), - 73 to Singular("You can enter any digits."), - 74 to Singular("Your PIN can have between 6 and 8 digits."), - 75 to Singular("Enter PIN"), - 76 to Singular("In demo mode, you can enter any PIN."), - 77 to Singular("Try again"), - 78 to Singular("Have your electronic medical card to hand."), - 79 to - Singular("The time it takes for your device to connect to the server can vary depending on the hardware and Internet speed."), - 80 to Singular("Failed to connect to the server."), - 81 to Singular("Check your connection to the Internet and start the process again."), - 84 to Singular("Incorrect PIN entered."), - 85 to Plurals( - mapOf( - Plurals.Type.One to - "You have %s attempt remaining before your card is locked.", - Plurals.Type.Other to - "You have %s attempts remaining before your card is locked." - ) - ), - 86 to Singular("Incorrect CAN entered"), - 87 to - Singular("You will find the access number in the top right-hand corner of your medical card."), - 88 to Singular("PIN was entered incorrectly several times."), - 89 to Singular("Your medical card needs to be unlocked with the PUK."), - 90 to Singular("Cancel"), - 91 to Singular("Searching for card..."), - 92 to Singular("Hold the medical card to the back of the device"), - 93 to Singular("Still searching ..."), - 94 to Singular("Slowly move the card on the back of the device."), - 95 to Singular("Tip"), - 96 to Singular("Device covers may make it difficult to connect via NFC."), - 97 to Singular("Card recognised"), - 98 to Singular("Try not to move the medical card."), - 103 to Singular("Medical card found. Please do not move."), - 104 to Singular("Connection interrupted"), - 105 to Singular("Hold your medical card to the back of the device again"), - 106 to Singular("You have successfully logged in"), - 107 to Singular("Note: only prescriptions from the last 100 days are downloaded."), - 108 to Singular("Demo mode enabled"), - 109 to Singular("Do you have an NFC-enabled medical card and want to try it out in demo mode?"), - 110 to Singular("Continue with card"), - 111 to Singular("Continue without a card"), - 112 to Singular("Demo mode enabled"), - 113 to Singular("Version: %s"), - 114 to Singular("Build hash: %s"), - 115 to Singular("Debug menu"), - 116 to Singular("Prescription code"), - 117 to Singular("Have this prescription code scanned at your pharmacy."), - 118 to Singular("This group code combines %s prescriptions"), - 119 to Singular("Redeem at pharmacy"), - 120 to Singular("You are in a pharmacy and want to redeem your prescription."), - 121 to Singular("Order or reserve"), - 122 to - Singular("Submit your prescription to a pharmacy and decide how you would like to receive your medication."), - 123 to Singular("You will need a valid medical card for this."), - 124 to Singular("Select pharmacy"), - 125 to Singular("e.g. Penguin Pharmacy or address"), - 126 to Singular("Find pharmacies easily"), - 127 to Singular("Share location and find pharmacies in your area"), - 128 to Singular("Share location"), - 129 to Singular("Open until %s o\'clock"), - 130 to Singular("Open continuously"), - 131 to Singular("Imprint"), - 132 to Singular("Publisher"), - 133 to Singular("gematik GmbH\nFriedrichstr. 136\n10117 Berlin, Germany"), - 134 to - Singular("Managing Director: Dr. med. Markus Leyck Dieken\nRegister Court: District Court of Berlin-Charlottenburg\nCommercial register no.: HRB 96351\nVAT ID: DE241843684"), - 135 to Singular("Responsible for the content"), - 136 to Singular("Dr. med. Markus Leyck Dieken"), - 137 to Singular("Contact"), - 140 to Singular("Note"), - 141 to - Singular("We strive to use gender-sensitive language. If you notice any errors, we would be pleased to hear from you by email."), - 142 to Singular("Scanned prescription"), - 143 to Singular("Medicine %s"), - 144 to Singular("Current"), - 145 to Singular("Update"), - 146 to Singular("Archive"), - 147 to Singular("You haven\'t redeemed any prescriptions yet"), - 148 to Plurals(mapOf(Plurals.Type.One to "%s medicine", Plurals.Type.Other to "%s medicines")), - 149 to Singular("Redeemed on %s"), - 150 to Singular("You haven\'t redeemed any prescriptions yet"), - 151 to Singular("Germany\'s modern platform for digital medicine"), - 152 to Singular("Write email"), - 153 to Singular("Open website"), - 154 to Singular("Welcome"), - 155 to Singular("Start login"), - 156 to Singular("Press Unlock"), - 159 to Singular("Unlock"), - 162 to Singular("Log in"), - 163 to Singular("Cancel"), - 164 to Singular("https://www.das-e-rezept-fuer-deutschland.de/"), - 165 to Singular("das-e-rezept-fuer-deutschland.de"), - 170 to Singular("Settings"), - 171 to Singular("Name unknown"), - 172 to Singular("Medical cards"), - 173 to Singular("Add card"), - 174 to Singular("To try out"), - 175 to - Singular("Demo mode allows you to explore all areas of the app even without an electronic medical card."), - 176 to Singular("Demo mode"), - 180 to Singular("Security"), - 181 to Singular("Protect your health information from unauthorised access."), - 185 to Singular("Legal information"), - 186 to Singular("Imprint"), - 187 to Singular("Privacy Policy"), - 188 to Singular("Terms of Use"), - 196 to Singular("Demo mode enabled"), - 197 to - Singular("Our demo mode shows you all the functions of the app – without a medical card."), - 198 to Singular("Would you like a tour of the app?"), - 199 to - Singular("Our demo mode shows you all the functions of the app – without a medical card."), - 200 to Singular("Launch demo mode"), - 201 to Singular("You do not have any current prescriptions"), - 202 to Singular("Secure prescription data"), - 203 to Singular("Improved protection of your data with a fingerprint or face scan."), - 204 to Singular("Enable now"), - 205 to Singular("Details"), - 206 to Singular("Keep track of things"), - 207 to - Singular("Mark this prescription as redeemed as soon as you have received your medication."), - 208 to Singular("Automatically update prescriptions"), - 209 to Singular("Log in so that your prescriptions can be automatically marked as redeemed."), - 210 to Singular("Log in now"), - 211 to Singular("Why am I only seeing this information?"), - 212 to Singular("Your health information is subject to special protection"), - 213 to Singular("Medicine %1\$d"), - 214 to Singular("Mark as redeemed"), - 215 to Singular("Mark as not redeemed"), - 216 to Singular("Delete from this device"), - 217 to Singular("Log"), - 218 to Singular("Scanned on"), - 219 to Singular("%1\$s o\'clock"), - 220 to Singular("Can still be redeemed until %s"), - 222 to Singular("Details about this medicine"), - 223 to Singular("Dosage form"), - 224 to Singular("Package size"), - 225 to Singular("Pharma central number (PZN)"), - 226 to Singular("Directions for use"), - 227 to - Singular("Please follow the directions for use in your medication schedule or the written dosage instructions from your doctor."), - 228 to Singular("Insured person"), - 229 to Singular("Name"), - 230 to Singular("Address"), - 231 to Singular("Date of birth"), - 232 to Singular("Health insurance / cost unit"), - 233 to Singular("Status"), - 234 to Singular("Insurance number"), - 235 to Singular("Prescriber"), - 236 to Singular("Name"), - 237 to Singular("Specialist physician"), - 238 to Singular("Physician number (LANR)"), - 239 to Singular("Institution"), - 240 to Singular("Name"), - 241 to Singular("Address"), - 242 to Singular("Establishment number"), - 243 to Singular("Telephone number"), - 244 to Singular("Email"), - 245 to Singular("Accident at work"), - 246 to Singular("Date of accident"), - 247 to Singular("Accident company or employer number"), - 248 to Singular("Do you want to permanently delete this prescription?"), - 249 to Singular("Delete"), - 250 to Singular("Cancel"), - 254 to Singular("This is a matter of urgency"), - 255 to - Singular("This medication can also be redeemed in a pharmacy at night without an emergency service fee."), - 256 to Singular("Substitute medication possible"), - 257 to - Singular("Substitutes are permitted. You may be given an alternative due to the legal requirements of your health insurance."), - 258 to Singular("Make a binding reservation"), - 259 to Singular("Request delivery service"), - 260 to Singular("Delivery by mail order"), - 262 to - Singular("Please note that prescribed medication may also be subject to additional payments."), - 263 to Singular("Opening hours"), - 264 to Singular("Website"), - 265 to Singular("Reservation"), - 266 to Singular("Redeem the following prescriptions with binding effect at %s?"), - 267 to Singular("Prescriptions"), - 268 to Singular("Redeem"), - 269 to Singular("Delivery service"), - 270 to Singular("Delivery address"), - 271 to Singular("How can we help you?"), - 272 to - Singular("Has the delivery address changed? Is there anything else you would like to tell the pharmacy?"), - 273 to Singular("Call now"), - 274 to Singular("Mail order"), - 275 to Singular("Log"), - 276 to Singular("without stored action"), - 278 to Singular("only valid today"), - 280 to Singular("Rename prescription block"), - 281 to Singular("You can assign a name for this prescription block."), - 282 to Singular("Log in"), - 283 to Singular("Log in"), - 284 to Singular("An NFC-enabled smartphone with at least Android 7"), - 285 to Singular("Enable NFC"), - 286 to - Singular("Please enable the NFC function on your device to log in with your medical card."), - 287 to Singular("Enable"), - 288 to Singular("How do I get a new medical card?"), - 289 to Singular("Your health insurance company will be able to help you with this."), - 290 to Singular("How do I get a PIN?"), - 291 to - Singular("You will receive a PIN for your medical card in a separate letter from your health insurance company."), - 292 to Singular("Would you like to save your login details for future logins?"), - 293 to Singular("Save login details"), - 294 to - Singular("Convenient and available to you soon: your data will be biometrically protected on the device for this purpose"), - 295 to Singular("Security not possible"), - 296 to Singular("No secure sensors available or biometric security not set up."), - 297 to Singular("Do not save login details"), - 298 to - Singular("Data-efficient: Requires you to enter your login details each time you launch the app"), - 299 to Singular("Repeat"), - 300 to Singular("To the homepage"), - 301 to Singular("Marked as redeemed on"), - 302 to Singular("Marked as not redeemed on"), - 303 to Singular("Display as single codes"), - 304 to Singular("Display as group code"), - 305 to Singular("%s of %s"), - 306 to Singular("Prescriptions redeemed?"), - 307 to Singular("Would you like to mark the prescriptions as redeemed?"), - 308 to Singular("Not redeemed"), - 309 to Singular("Redeemed"), - 310 to Singular("Opens at %s o\'clock"), - 311 to Singular("+49 800 277 377 7"), - 312 to Singular("Technical hotline"), - 313 to Singular("Open scanner for prescriptions"), - 314 to Singular("Settings"), - 315 to Singular("+49 800 277 377 7"), - 316 to Singular("Please identify yourself via fingerprint or facial recognition."), - 317 to Singular("Note"), - 318 to Singular("This change will only take effect after the app is restarted."), - 319 to Singular("OK"), - 320 to Singular("Tracking"), - 321 to - Singular("Help us make this app better. All user data is collected anonymously and is used solely to improve the user experience."), - 322 to Singular("Allow tracking"), - 323 to - Singular("In the event of a crash or an error in the app, the app sends us information about the reasons along with the operating system version and details of the hardware used."), - 324 to Singular("Suppress screenshots"), - 325 to Singular("Prevents the display of a preview image when switching apps"), - 326 to - Singular("Do you consent to the anonymous analysis of usage behaviour by e-prescription?"), - 327 to - Singular("This includes information about your phone\'s hardware and software, settings of the e-prescription app as well as the extent of use, but never any personal or health data concerning you. \nThis data is made available exclusively to gematik GmbH by data processors and is deleted after 180 days at the latest. You can disable the analysis of your usage behaviour at any time in the settings menu of the app.\nWe can use this data to understand which functions are used frequently and improve them. Furthermore, we can assess how long older technology needs to be supported and when we can, for example, make a newer operating system version mandatory without affecting (too many) users."), - 328 to Singular("Allow"), - 340 to Plurals( - mapOf( - Plurals.Type.One to "You have been prescribed %s medicine", - Plurals.Type.Other to "You have been prescribed %s medicines" - ) - ), - 341 to Singular("Tap here to redeem at a pharmacy"), - 342 to Singular("Redeem now"), - 343 to Singular("Show all"), - 344 to Singular("Delete regulation"), - 345 to Singular("Mark as redeemed"), - 346 to Singular("Undo"), - 347 to Singular("Show more"), - 348 to Singular("Show less"), - 349 to Singular("Technical information"), - 350 to Singular("Log out"), - 351 to - Singular("All access data to the health network will be deleted. Your prescription data will be retained."), - 352 to Singular("This will delete your login details."), - 353 to Singular("Log out"), - 354 to Singular("Cancel"), - 355 to Singular("Would you like to log out of the app?"), - 356 to Singular("Security of your prescription data"), - 357 to - Singular("Please be aware that people with whom you may share this device and whose biometrics may be stored on this device or who have the device PIN, swipe pattern or password may also have access to your prescriptions."), - 358 to Singular("Redeem with binding effect?"), - 359 to - Singular("Your prescriptions will be sent to this pharmacy. You will then not be able to redeem them in any other pharmacy."), - 360 to Singular("Cancel"), - 361 to Singular("Redeem now"), - 362 to Singular("Successfully redeemed"), - 363 to - Singular("The pharmacy will contact you as soon as possible to verify the delivery details with you."), - 364 to Singular("Complete your order in the browser"), - 365 to Singular("Go to homepage"), - 366 to - Singular("The mail-order pharmacy will create a shopping cart for you with your medicines. This process may take a few minutes."), - 367 to - Singular("Tap on \"Open shopping cart\" and complete your order on the pharmacy\'s website."), - 368 to Singular("To the homepage"), - 369 to Singular("Sending failed"), - 370 to Singular("Repeat"), - 371 to - Singular("Your order will usually be ready for you as soon as possible. Please contact the pharmacy for exact timings."), - 372 to Singular("Your shopping cart is ready"), - 373 to Singular("Collection code received"), - 374 to Singular("Message received"), - 375 to Singular("Show collection code"), - 376 to Singular("Open shopping cart"), - 377 to Singular("Show this code at your pharmacy."), - 378 to Singular("Collection code"), - 379 to Singular("No messages"), - 380 to Singular("You haven\'t received any messages yet"), - 381 to - Singular("Unfortunately, your pharmacy\'s message was empty. Please contact your pharmacy."), - 382 to Singular("No email program set up"), - 383 to Singular("No results"), - 384 to Singular("We couldn\'t find any results with this search term."), - 388 to Singular("Open source licences"), - 389 to Singular("Contact"), - 390 to Singular("Call technical hotline"), - 391 to Singular("Write email"), - 392 to Singular("Take part in the survey"), - 394 to Singular("+49 800 277 377 7"), - 467 to Singular("E-Rezept"), - 448 to - Singular("Dear Service Team, I received a message from a pharmacy. Unfortunately, however, I could not tell my user the message because I did not understand it. Please check what happened here and help us. Thank you very much! The e-prescription app"), - 2 to stringsDe.understand, - 10 to stringsDe.onBoardingPage1Headline, - 31 to stringsDe.consistentPassword, - 39 to stringsDe.presBottombarPharmacies, - 41 to stringsDe.prescriptionItemExpirationDaysNew, - 42 to stringsDe.prescriptionItemAcceptDays, - 59 to stringsDe.camAcceptMlkitTitle, - 60 to stringsDe.camAcceptMlkitBody, - 61 to stringsDe.camAcceptMlkitAccept, - 62 to stringsDe.camAcceptMlkitDecline, - 68 to stringsDe.cdwIntroWhatYouNeedNoEgk, - 82 to stringsDe.cdwNfcErrorTitleInvalidCertificate, - 83 to stringsDe.cdwNfcErrorBodyInvalidCertificate, - 99 to stringsDe.cdwNfcCommunicationHeadlineTrustedChannelEstablished, - 100 to stringsDe.cdwNfcCommunicationHeadlineCertificateLoaded, - 101 to stringsDe.cdwNfcCommunicationHeadlinePinVerified, - 102 to stringsDe.cdwNfcCommunicationHeadlineChallengeSigned, - 138 to stringsDe.menuLegalNoticeUrl, - 139 to stringsDe.legalNoticeEmail, - 157 to stringsDe.authSubtitleError, - 158 to stringsDe.authInfoError, - 160 to stringsDe.authMoreHotline, - 161 to stringsDe.authMoreWeb, - 166 to stringsDe.authPromptEnterPassword, - 167 to stringsDe.authPromptCheckPassword, - 168 to stringsDe.authErrorFailedAuthsHeadline, - 169 to stringsDe.authErrorFailedAuthsInfo, - 177 to stringsDe.settingsAccessibilityHeadline, - 178 to stringsDe.settingsAccessibilityZoomToggle, - 179 to stringsDe.settingsAccessibilityZoomInfo, - 182 to stringsDe.settingsAppprotectionDeviceSecurityHeader, - 183 to stringsDe.settingsAppprotectionDeviceSecurityInfo, - 184 to stringsDe.settingsAppprotectionDeviceSecurityDisabledInfo, - 189 to stringsDe.tokenHeadline, - 190 to stringsDe.accessTokenTitle, - 191 to stringsDe.singleSignOnTokenTitle, - 192 to stringsDe.noAccessToken, - 193 to stringsDe.noSingleSignOnToken, - 194 to stringsDe.copied, - 195 to stringsDe.copyContentDescription, - 221 to stringsDe.presDetailMedicationExpiryUntil, - 251 to stringsDe.presDetailUnRedeemMsg, - 252 to stringsDe.presDetailUnRedeemAll, - 253 to stringsDe.presDetailUnRedeemSelected, - 261 to stringsDe.pharmDetailHintHeader, - 277 to stringsDe.prescriptionItemAcceptOnlyToday, - 279 to stringsDe.prescriptionItemExpired, - 329 to stringsDe.settingsDeviceSecurityAllow, - 330 to stringsDe.settingsAppprotectionModePasswordHeadline, - 331 to stringsDe.settingsAppprotectionModePasswordInfo, - 332 to stringsDe.settingsPasswordHeadline, - 333 to stringsDe.settingsPasswordSave, - 334 to stringsDe.settingsPasswordAccShowPasswordToggle, - 335 to stringsDe.settingsPasswordEnterPassword, - 336 to stringsDe.settingsPasswordHint, - 337 to stringsDe.settingsPasswordRepeatPassword, - 338 to stringsDe.settingsPasswordStrength, - 339 to stringsDe.settingsPasswordSuggestions, - 385 to stringsDe.searchPharmacyErrorTitle, - 386 to stringsDe.searchPharmacyErrorSubtitle, - 387 to stringsDe.searchPharmacyErrorAction, - 393 to stringsDe.settingsContactFeedbackForm, - 395 to stringsDe.settingsContactMailAddress, - 396 to stringsDe.settingsContactFeedbackAdress, - 397 to stringsDe.settingsShowToken, - 398 to stringsDe.learnMoreBtn, - 399 to stringsDe.onBoardingPage1AccImage, - 400 to stringsDe.onBoardingPage2AccImage, - 401 to stringsDe.onBoardingPage3AccImage, - 402 to stringsDe.onBoardingPage5Header, - 403 to stringsDe.onBoardingPage5SubHeader, - 404 to stringsDe.onBoardingPage5Info1, - 405 to stringsDe.onBoardingPage5Info2, - 406 to stringsDe.onBoardingPage5Info3, - 407 to stringsDe.onBoardingPage5Label, - 408 to stringsDe.onBoardingPage5LabelInfo, - 409 to stringsDe.onBoardingPage5Anonym, - 410 to stringsDe.onBoardingPage5Next, - 411 to stringsDe.onBoardingSecureAppPageHeader, - 412 to stringsDe.onBoardingSecureAppPageInfo, - 413 to stringsDe.onboardingSecureAppOr, - 414 to stringsDe.onboardingSecureAppButtonBest, - 415 to stringsDe.onboardingSecureAppButtonBestChosen, - 416 to stringsDe.onboardingSecureAppButtonBestInfo, - 417 to stringsDe.settingsTrackingDialogText1, - 418 to stringsDe.settingsTrackingDialogText2, - 419 to stringsDe.settingsTrackingDialogText3, - 420 to stringsDe.settingsTrackingAllowTitle, - 421 to stringsDe.settingsTrackingNotAllow, - 422 to stringsDe.settingsTrackingDisallowInfo, - 423 to stringsDe.settingsTrackingAllowInfo, - 424 to stringsDe.settingsTrackingAllowEmoji, - 425 to stringsDe.settingsFeedbackFormHeader, - 426 to stringsDe.settingsFeedbackFormPlaceholder, - 427 to stringsDe.seetingsFeedbackFormAdditionalDataInfo, - 428 to stringsDe.seetingsFeedbackFormAdditionalDataOs, - 429 to stringsDe.seetingsFeedbackFormAdditionalDataOsDetail, - 430 to stringsDe.seetingsFeedbackFormAdditionalDataDevice, - 431 to stringsDe.seetingsFeedbackFormAdditionalDataDeviceDetail, - 432 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmode, - 433 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmodeOn, - 434 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmodeOff, - 435 to stringsDe.seetingsFeedbackFormAdditionalDataLanguage, - 436 to stringsDe.settingsFeedbackFormSend, - 437 to stringsDe.settingsFeedbackFormHeadline, - 438 to stringsDe.presDetailMedicationRedeemButtonText, - 439 to stringsDe.redeemSyncedPrescriptionsDialogHeader, - 440 to stringsDe.redeemSyncedPrescriptionsDialogInfo, - 441 to stringsDe.redeemSyncedPrescriptionsDialogOk, - 442 to stringsDe.logoutDeleteNoAccess, - 443 to stringsDe.communicationErrorActionText, - 444 to stringsDe.communicationErrorInboxHeader, - 445 to stringsDe.communicationErrorInboxDisplayText, - 446 to stringsDe.messagesContactMailAddress, - 447 to stringsDe.messagesContactEmailSubject, - 449 to stringsDe.messagesContactEmailDataTransparency, - 450 to stringsDe.messagesContactEmailErrorCode, - 451 to stringsDe.insecureDeviceTitle, - 452 to stringsDe.insecureDeviceHeader, - 453 to stringsDe.insecureDeviceInfo, - 454 to stringsDe.insecureDeviceAccept, - 455 to stringsDe.alternateAuthHeader, - 456 to stringsDe.alternateAuthInfo, - 457 to stringsDe.presDetailMedicationRedeemedOn, - 458 to stringsDe.presDetailSubstitutedHeader, - 459 to stringsDe.presDetailSubstitutedInfo, - 460 to stringsDe.pharmacyDetailDataInfo, - 461 to stringsDe.pharmacyDetailDataInfoDomain, - 462 to stringsDe.pharmacyDetailDataInfoBtn, - 463 to stringsDe.pharmacyDetailPharmacyPortalUri, - 464 to stringsDe.pharmacyDetailDataInfoFaqsUri, - 465 to stringsDe.pharmacyDetailNotReadyHeader, - 466 to stringsDe.pharmacyDetailNotReadyInfo, - 468 to stringsDe.searchPharmaciesFilterReady, - 469 to stringsDe.searchPharmaciesFilterOpenNow, - 470 to stringsDe.searchPharmaciesFilterDeliveryService, - 471 to stringsDe.searchPharmaciesFilterOnlineService, - 472 to stringsDe.searchPharmaciesFilterHeader, - 473 to stringsDe.searchPharmaciesFilterSectionFavorites, - 474 to stringsDe.searchPharmaciesFilter, - 475 to stringsDe.searchPharmaciesLocationNaInfo, - 476 to stringsDe.searchPharmaciesLocationNaHeader, - 477 to stringsDe.errorMessageNetworkNotAvailable, - 478 to stringsDe.errorMessageServerCommunicationFailed, - 479 to stringsDe.errorMessageVauError, - 480 to stringsDe.settingsNoActiveToken, - 481 to stringsDe.prescriptionDetailRedeemed, - 482 to stringsDe.prescriptionDetailUnRedeemed, - 483 to stringsDe.insecureDeviceTitleSafetynet, - 484 to stringsDe.insecureDeviceHeaderSafetynet, - 485 to stringsDe.insecureDeviceInfoSafetynet, - 486 to stringsDe.insecureDeviceAcceptSafetynet, - 487 to stringsDe.insecureDeviceSafetynetMoreInfo, - 488 to stringsDe.insecureDeviceSafetynetLinkText, - 489 to stringsDe.insecureDeviceSafetynetLink, - 490 to stringsDe.cdwHealthCardInfoTitle, - 491 to stringsDe.cdwHealthcardInfoHeadline, - 492 to stringsDe.cdwHealthcardInfo, - 493 to stringsDe.cdwHealthcardInfoBullet, - 494 to stringsDe.cdwHealthcardInfoCan, - 495 to stringsDe.cdwHealthcardInfoCard, - 496 to stringsDe.cdwHealthcardInfoPin, - 497 to stringsDe.cdwHealthcardInfoPinPin, - 498 to stringsDe.cdwHealthCardInfoHintDescription, - 499 to stringsDe.cdwHealthcardInfoMailDescription, - 500 to stringsDe.cdwHealthcardInfoMail, - 501 to stringsDe.cdwRegisterWithHealthyCard, - 502 to stringsDe.cdwRegisterWithHealthInsurance, - 503 to stringsDe.cdwRegisterHealtyCardInfo, - 504 to stringsDe.cdwRegisterHealthInsuranceInfo, - 505 to stringsDe.cdwRegisterTitle, - 506 to stringsDe.cdwRegisterBody, - 507 to stringsDe.cdwRegisterScreenTitle, - 508 to stringsDe.cdwManRegisterAccessibility, - 509 to stringsDe.cdwWomanRegisterAccessibility, - 510 to stringsDe.cdwNoNfc, - 511 to stringsDe.profileEditName, - 512 to stringsDe.profileEditNameInfo, - 513 to stringsDe.profileEditNamePlaceHolder, - 514 to stringsDe.settingsProfileEditName, - 515 to stringsDe.settingsProfileDelete, - 516 to stringsDe.settingsProfilesHeadline, - 517 to stringsDe.onboardingProfileHeader, - 518 to stringsDe.onboardingProfileInfo, - 519 to stringsDe.onboardingProfileInputName, - 520 to stringsDe.settingsAddProfilesHintTitle, - 521 to stringsDe.settingsAddProfilesHintInfo, - 522 to stringsDe.settingsAddProfile, - 523 to stringsDe.profileSetupSave, - 524 to stringsDe.cdwHealthInsurancePageTitle, - 525 to stringsDe.cdwHealthInsuranceTitle, - 526 to stringsDe.cdwHealthInsuranceBodyWhatYouNeed, - 527 to stringsDe.cdwHealthInsuranceBodyHowToGet, - 528 to stringsDe.cdwHealthInsuranceCaptionRecognizeHealthcard, - 529 to stringsDe.cdwHealthInsuranceLearnMore, - 530 to stringsDe.cdwHealthInsuranceSelectCompany, - 531 to stringsDe.cdwHealthInsuranceNoCompanySelected, - 532 to stringsDe.cdwHealthInsuranceWhatToDo, - 533 to stringsDe.cdwHealthInsuranceNoCantactsTitle, - 534 to stringsDe.cdwHealthInsuranceNoCantactsBody, - 535 to stringsDe.cdwHealthInsuranceContactHealthcardPin, - 536 to stringsDe.cdwHealthInsuranceContactPinOnly, - 537 to stringsDe.cdwHealthInsuranceContactInsuranceCompany, - 538 to stringsDe.cdwHealthInsuranceMailSubject, - 539 to stringsDe.settingsHealthInsuranceContactTitle, - 540 to stringsDe.settingsHealthInsuranceContactBody, - 541 to stringsDe.settingsHealthInsuranceContactAction, - 542 to stringsDe.cdwCapabilityTitle, - 543 to stringsDe.cdwCapabilityHeadline, - 544 to stringsDe.cdwCapabilityBody, - 545 to stringsDe.cdwCapabilityMore, - 546 to stringsDe.editProfileEmptyProfileName, - 547 to stringsDe.editProfileDuplicatedProfileName, - 548 to stringsDe.editProfileTitle, - 549 to stringsDe.editProfileColorSelected, - 550 to stringsDe.editProfileBackgroundColor, - 551 to stringsDe.profileColorNameGray, - 552 to stringsDe.profileColorSunDew, - 553 to stringsDe.profileColorNamePink, - 554 to stringsDe.profileColorNameTree, - 555 to stringsDe.profileColorNameMoon, - 556 to stringsDe.settingsProfileNotConnected, - 557 to stringsDe.settingsProfileConnected, - 558 to stringsDe.settingsProfileLastAuthenticatedOn, - 559 to stringsDe.removeProfileHeader, - 560 to stringsDe.removeProfileDetailMessage, - 561 to stringsDe.removeProfileYes, - 562 to stringsDe.removeProfileNo, - 563 to stringsDe.removeProfile, - 564 to stringsDe.profileEditNameForDefault, - 565 to stringsDe.profileEditNameForDefaultInfo, - 566 to stringsDe.cdwNfcErrorTitleInvalidOcspResponseOfHealthCardCertificate, - 567 to stringsDe.cdwNfcErrorBodyInvalidOcspResponseOfHealthCardCertificate, - 568 to stringsDe.loginDescription, - 569 to stringsDe.presDetailHealthPortalDescription, - 570 to stringsDe.presDetailHealthPortalDescriptionUrlInfo, - 571 to stringsDe.presDetailHealthPortalDescriptionUrl, - 572 to stringsDe.selectProfile, - 573 to stringsDe.editProfiles, - 574 to stringsDe.zeroPrescriptionsUpdatet, - 575 to stringsDe.prescriptionsUpdated, - 576 to stringsDe.prescriptionStatusReady, - 577 to stringsDe.prescriptionStatusInProgress, - 578 to stringsDe.prescriptionStatusCompleted, - 579 to stringsDe.prescriptionStatusUnknown, - 580 to stringsDe.pharmacyDetailTitle, - 581 to stringsDe.settingsShowAuditEvents, - 582 to stringsDe.settingsShowAuditEventsInfo, - 583 to stringsDe.settingsShowTokenInfo, - 584 to stringsDe.autiteventsHeadline, - 585 to stringsDe.logoutProfile, - 586 to stringsDe.loginProfile, - 587 to stringsDe.functionNotAvaillableOnDemoMode, - 588 to stringsDe.presDetailMedicationInProgress, - 589 to stringsDe.directAssignmentWillBeForwardet, - 590 to stringsDe.noAuditEventsHeader, - 591 to stringsDe.noAuditEventsNotAuthenticatedInfo, - 592 to stringsDe.noAuditEventsEmptyProtocolListInfo, - 593 to stringsDe.auditEventsUpdatedAt, - 594 to stringsDe.logoutDeleteInProgress, - 595 to stringsDe.profileNotConnected, - 596 to stringsDe.profileConnected, - 597 to stringsDe.connectProfileHeader, - 598 to stringsDe.connectProfileInfo, - 599 to stringsDe.connectProfileConnect, - 600 to stringsDe.noTokenAvailableInDemoMode, - 601 to stringsDe.settingsAddProfileNotAllowed, - 602 to stringsDe.desktopMenuInfo, - 603 to stringsDe.desktopMenuTerms, - 604 to stringsDe.desktopMenuData, - 605 to stringsDe.desktopMainWelcomeTitle, - 606 to stringsDe.desktopMainWelcomeSubtitle, - 607 to stringsDe.desktopMainLoginWithHealthcard, - 608 to stringsDe.desktopMainPrescriptions, - 609 to stringsDe.desktopMainCommunications, - 610 to stringsDe.desktopMainRedeemablePrescriptions, - 611 to stringsDe.desktopMainRedeemedPrescriptions, - 612 to stringsDe.desktopMainPharmacyCommunications, - 613 to stringsDe.desktopMainPharmacyProtocol, - 614 to stringsDe.desktopMainRefresh, - 615 to stringsDe.desktopMainZoomIn, - 616 to stringsDe.desktopMainZoomOut, - 617 to stringsDe.desktopMainHelp, - 618 to stringsDe.desktopMainLogout, - 619 to stringsDe.desktopPrescriptionNoData, - 620 to stringsDe.desktopLoginPageDataTerms, - 621 to stringsDe.desktopLoginPageConnectReader, - 622 to stringsDe.desktopLoginPageEnterCan, - 623 to stringsDe.desktopLoginPageEnterPin, - 624 to stringsDe.desktopLoginPageConnectHealthcard, - 625 to stringsDe.desktopLoginPageReaderSearchHealthcard, - 626 to stringsDe.desktopLoginPageReaderFoundHealthcard, - 627 to stringsDe.desktopLoginPageReaderError, - 628 to stringsDe.desktopLoginPageConnectHealthcardErrorIoTitle, - 629 to stringsDe.desktopLoginPageConnectHealthcardErrorIoSubtitle, - 630 to stringsDe.desktopPrescriptionExpiresOn, - 631 to stringsDe.desktopPrescriptionAcceptUntil, - 632 to stringsDe.desktopPrescriptionExpired, - 633 to stringsDe.desktopPrescriptionPrescribedOn, - 634 to stringsDe.desktopPrescriptionShowDmc, - 635 to stringsDe.desktopCommunicationOnPremise, - 636 to stringsDe.desktopCommunicationDelivery, - 637 to stringsDe.desktopCommunicationShipment, - 638 to stringsDe.desktopTermsLink, - 639 to stringsDe.desktopDataLink, - 640 to stringsDe.desktopHelpLink, - 641 to stringsDe.desktopContactLink, - 642 to stringsDe.kbvMemberStatus1, - 643 to stringsDe.kbvMemberStatus3, - 644 to stringsDe.kbvMemberStatus5, - 645 to stringsDe.kbvNormSizeKa, - 646 to stringsDe.kbvNormSizeKtp, - 647 to stringsDe.kbvNormSizeN1, - 648 to stringsDe.kbvNormSizeN2, - 649 to stringsDe.kbvNormSizeN3, - 650 to stringsDe.kbvNormSizeNb, - 651 to stringsDe.kbvNormSizeSonstiges, - 652 to stringsDe.kbvCodeDosageFormAeo, - 653 to stringsDe.kbvCodeDosageFormAmp, - 654 to stringsDe.kbvCodeDosageFormApa, - 655 to stringsDe.kbvCodeDosageFormAsn, - 656 to stringsDe.kbvCodeDosageFormAso, - 657 to stringsDe.kbvCodeDosageFormAto, - 658 to stringsDe.kbvCodeDosageFormAtr, - 659 to stringsDe.kbvCodeDosageFormAub, - 660 to stringsDe.kbvCodeDosageFormAuc, - 661 to stringsDe.kbvCodeDosageFormAug, - 662 to stringsDe.kbvCodeDosageFormAus, - 663 to stringsDe.kbvCodeDosageFormBad, - 664 to stringsDe.kbvCodeDosageFormBal, - 665 to stringsDe.kbvCodeDosageFormBan, - 666 to stringsDe.kbvCodeDosageFormBeu, - 667 to stringsDe.kbvCodeDosageFormBin, - 668 to stringsDe.kbvCodeDosageFormBon, - 669 to stringsDe.kbvCodeDosageFormBpl, - 670 to stringsDe.kbvCodeDosageFormBre, - 671 to stringsDe.kbvCodeDosageFormBta, - 672 to stringsDe.kbvCodeDosageFormCre, - 673 to stringsDe.kbvCodeDosageFormDfl, - 674 to stringsDe.kbvCodeDosageFormDil, - 675 to stringsDe.kbvCodeDosageFormDis, - 676 to stringsDe.kbvCodeDosageFormDka, - 677 to stringsDe.kbvCodeDosageFormDos, - 678 to stringsDe.kbvCodeDosageFormDra, - 679 to stringsDe.kbvCodeDosageFormDrm, - 680 to stringsDe.kbvCodeDosageFormDsc, - 681 to stringsDe.kbvCodeDosageFormDss, - 682 to stringsDe.kbvCodeDosageFormEdp, - 683 to stringsDe.kbvCodeDosageFormEin, - 684 to stringsDe.kbvCodeDosageFormEle, - 685 to stringsDe.kbvCodeDosageFormEli, - 686 to stringsDe.kbvCodeDosageFormEmu, - 687 to stringsDe.kbvCodeDosageFormEss, - 688 to stringsDe.kbvCodeDosageFormEsu, - 689 to stringsDe.kbvCodeDosageFormExt, - 690 to stringsDe.kbvCodeDosageFormFbe, - 691 to stringsDe.kbvCodeDosageFormFbw, - 692 to stringsDe.kbvCodeDosageFormFda, - 693 to stringsDe.kbvCodeDosageFormFer, - 694 to stringsDe.kbvCodeDosageFormFet, - 695 to stringsDe.kbvCodeDosageFormFla, - 696 to stringsDe.kbvCodeDosageFormFle, - 697 to stringsDe.kbvCodeDosageFormFlu, - 698 to stringsDe.kbvCodeDosageFormFmr, - 699 to stringsDe.kbvCodeDosageFormFol, - 700 to stringsDe.kbvCodeDosageFormFrb, - 701 to stringsDe.kbvCodeDosageFormFse, - 702 to stringsDe.kbvCodeDosageFormFta, - 703 to stringsDe.kbvCodeDosageFormGek, - 704 to stringsDe.kbvCodeDosageFormGel, - 705 to stringsDe.kbvCodeDosageFormGli, - 706 to stringsDe.kbvCodeDosageFormGlo, - 707 to stringsDe.kbvCodeDosageFormGmr, - 708 to stringsDe.kbvCodeDosageFormGpa, - 709 to stringsDe.kbvCodeDosageFormGra, - 710 to stringsDe.kbvCodeDosageFormGse, - 711 to stringsDe.kbvCodeDosageFormGul, - 712 to stringsDe.kbvCodeDosageFormHas, - 713 to stringsDe.kbvCodeDosageFormHkm, - 714 to stringsDe.kbvCodeDosageFormHkp, - 715 to stringsDe.kbvCodeDosageFormHpi, - 716 to stringsDe.kbvCodeDosageFormHvw, - 717 to stringsDe.kbvCodeDosageFormIfa, - 718 to stringsDe.kbvCodeDosageFormIfb, - 719 to stringsDe.kbvCodeDosageFormIfd, - 720 to stringsDe.kbvCodeDosageFormIfe, - 721 to stringsDe.kbvCodeDosageFormIff, - 722 to stringsDe.kbvCodeDosageFormIfk, - 723 to stringsDe.kbvCodeDosageFormIfl, - 724 to stringsDe.kbvCodeDosageFormIfs, - 725 to stringsDe.kbvCodeDosageFormIha, - 726 to stringsDe.kbvCodeDosageFormIhp, - 727 to stringsDe.kbvCodeDosageFormIie, - 728 to stringsDe.kbvCodeDosageFormIil, - 729 to stringsDe.kbvCodeDosageFormIim, - 730 to stringsDe.kbvCodeDosageFormIka, - 731 to stringsDe.kbvCodeDosageFormIlo, - 732 to stringsDe.kbvCodeDosageFormImp, - 733 to stringsDe.kbvCodeDosageFormInf, - 734 to stringsDe.kbvCodeDosageFormInh, - 735 to stringsDe.kbvCodeDosageFormIni, - 736 to stringsDe.kbvCodeDosageFormInl, - 737 to stringsDe.kbvCodeDosageFormIns, - 738 to stringsDe.kbvCodeDosageFormIst, - 739 to stringsDe.kbvCodeDosageFormIsu, - 740 to stringsDe.kbvCodeDosageFormIup, - 741 to stringsDe.kbvCodeDosageFormKan, - 742 to stringsDe.kbvCodeDosageFormKap, - 743 to stringsDe.kbvCodeDosageFormKat, - 744 to stringsDe.kbvCodeDosageFormKda, - 745 to stringsDe.kbvCodeDosageFormKeg, - 746 to stringsDe.kbvCodeDosageFormKer, - 747 to stringsDe.kbvCodeDosageFormKgu, - 748 to stringsDe.kbvCodeDosageFormKid, - 749 to stringsDe.kbvCodeDosageFormKii, - 750 to stringsDe.kbvCodeDosageFormKks, - 751 to stringsDe.kbvCodeDosageFormKli, - 752 to stringsDe.kbvCodeDosageFormKlt, - 753 to stringsDe.kbvCodeDosageFormKmp, - 754 to stringsDe.kbvCodeDosageFormKmr, - 755 to stringsDe.kbvCodeDosageFormKod, - 756 to stringsDe.kbvCodeDosageFormKom, - 757 to stringsDe.kbvCodeDosageFormKon, - 758 to stringsDe.kbvCodeDosageFormKpg, - 759 to stringsDe.kbvCodeDosageFormKri, - 760 to stringsDe.kbvCodeDosageFormKss, - 761 to stringsDe.kbvCodeDosageFormKsu, - 762 to stringsDe.kbvCodeDosageFormKta, - 763 to stringsDe.kbvCodeDosageFormLan, - 764 to stringsDe.kbvCodeDosageFormLii, - 765 to stringsDe.kbvCodeDosageFormLiq, - 766 to stringsDe.kbvCodeDosageFormLoe, - 767 to stringsDe.kbvCodeDosageFormLot, - 768 to stringsDe.kbvCodeDosageFormLov, - 769 to stringsDe.kbvCodeDosageFormLse, - 770 to stringsDe.kbvCodeDosageFormLta, - 771 to stringsDe.kbvCodeDosageFormLup, - 772 to stringsDe.kbvCodeDosageFormLut, - 773 to stringsDe.kbvCodeDosageFormMil, - 774 to stringsDe.kbvCodeDosageFormMis, - 775 to stringsDe.kbvCodeDosageFormMix, - 776 to stringsDe.kbvCodeDosageFormMrg, - 777 to stringsDe.kbvCodeDosageFormMrp, - 778 to stringsDe.kbvCodeDosageFormMta, - 779 to stringsDe.kbvCodeDosageFormMuw, - 780 to stringsDe.kbvCodeDosageFormNag, - 781 to stringsDe.kbvCodeDosageFormNao, - 782 to stringsDe.kbvCodeDosageFormNas, - 783 to stringsDe.kbvCodeDosageFormNaw, - 784 to stringsDe.kbvCodeDosageFormNds, - 785 to stringsDe.kbvCodeDosageFormNsa, - 786 to stringsDe.kbvCodeDosageFormNtr, - 787 to stringsDe.kbvCodeDosageFormOcu, - 788 to stringsDe.kbvCodeDosageFormOel, - 789 to stringsDe.kbvCodeDosageFormOht, - 790 to stringsDe.kbvCodeDosageFormOvu, - 791 to stringsDe.kbvCodeDosageFormPam, - 792 to stringsDe.kbvCodeDosageFormPas, - 793 to stringsDe.kbvCodeDosageFormPel, - 794 to stringsDe.kbvCodeDosageFormPen, - 795 to stringsDe.kbvCodeDosageFormPer, - 796 to stringsDe.kbvCodeDosageFormPfl, - 797 to stringsDe.kbvCodeDosageFormPft, - 798 to stringsDe.kbvCodeDosageFormPhi, - 799 to stringsDe.kbvCodeDosageFormPhv, - 800 to stringsDe.kbvCodeDosageFormPie, - 801 to stringsDe.kbvCodeDosageFormPif, - 802 to stringsDe.kbvCodeDosageFormPii, - 803 to stringsDe.kbvCodeDosageFormPij, - 804 to stringsDe.kbvCodeDosageFormPik, - 805 to stringsDe.kbvCodeDosageFormPis, - 806 to stringsDe.kbvCodeDosageFormPiv, - 807 to stringsDe.kbvCodeDosageFormPki, - 808 to stringsDe.kbvCodeDosageFormPle, - 809 to stringsDe.kbvCodeDosageFormPlf, - 810 to stringsDe.kbvCodeDosageFormPlg, - 811 to stringsDe.kbvCodeDosageFormPlh, - 812 to stringsDe.kbvCodeDosageFormPli, - 813 to stringsDe.kbvCodeDosageFormPlk, - 814 to stringsDe.kbvCodeDosageFormPls, - 815 to stringsDe.kbvCodeDosageFormPlv, - 816 to stringsDe.kbvCodeDosageFormPpl, - 817 to stringsDe.kbvCodeDosageFormPrs, - 818 to stringsDe.kbvCodeDosageFormPse, - 819 to stringsDe.kbvCodeDosageFormPst, - 820 to stringsDe.kbvCodeDosageFormPud, - 821 to stringsDe.kbvCodeDosageFormPul, - 822 to stringsDe.kbvCodeDosageFormRed, - 823 to stringsDe.kbvCodeDosageFormRek, - 824 to stringsDe.kbvCodeDosageFormRet, - 825 to stringsDe.kbvCodeDosageFormRgr, - 826 to stringsDe.kbvCodeDosageFormRka, - 827 to stringsDe.kbvCodeDosageFormRms, - 828 to stringsDe.kbvCodeDosageFormRsc, - 829 to stringsDe.kbvCodeDosageFormRsu, - 830 to stringsDe.kbvCodeDosageFormRut, - 831 to stringsDe.kbvCodeDosageFormSaf, - 832 to stringsDe.kbvCodeDosageFormSal, - 833 to stringsDe.kbvCodeDosageFormSam, - 834 to stringsDe.kbvCodeDosageFormSch, - 835 to stringsDe.kbvCodeDosageFormSei, - 836 to stringsDe.kbvCodeDosageFormSha, - 837 to stringsDe.kbvCodeDosageFormSir, - 838 to stringsDe.kbvCodeDosageFormSlz, - 839 to stringsDe.kbvCodeDosageFormSmf, - 840 to stringsDe.kbvCodeDosageFormSmt, - 841 to stringsDe.kbvCodeDosageFormSmu, - 842 to stringsDe.kbvCodeDosageFormSpa, - 843 to stringsDe.kbvCodeDosageFormSpf, - 844 to stringsDe.kbvCodeDosageFormSpl, - 845 to stringsDe.kbvCodeDosageFormSpr, - 846 to stringsDe.kbvCodeDosageFormSpt, - 847 to stringsDe.kbvCodeDosageFormSri, - 848 to stringsDe.kbvCodeDosageFormSsu, - 849 to stringsDe.kbvCodeDosageFormSta, - 850 to stringsDe.kbvCodeDosageFormStb, - 851 to stringsDe.kbvCodeDosageFormSti, - 852 to stringsDe.kbvCodeDosageFormStr, - 853 to stringsDe.kbvCodeDosageFormSub, - 854 to stringsDe.kbvCodeDosageFormSue, - 855 to stringsDe.kbvCodeDosageFormSul, - 856 to stringsDe.kbvCodeDosageFormSup, - 857 to stringsDe.kbvCodeDosageFormSus, - 858 to stringsDe.kbvCodeDosageFormSut, - 859 to stringsDe.kbvCodeDosageFormSuv, - 860 to stringsDe.kbvCodeDosageFormSwa, - 861 to stringsDe.kbvCodeDosageFormTab, - 862 to stringsDe.kbvCodeDosageFormTae, - 863 to stringsDe.kbvCodeDosageFormTam, - 864 to stringsDe.kbvCodeDosageFormTee, - 865 to stringsDe.kbvCodeDosageFormTei, - 866 to stringsDe.kbvCodeDosageFormTes, - 867 to stringsDe.kbvCodeDosageFormTin, - 868 to stringsDe.kbvCodeDosageFormTka, - 869 to stringsDe.kbvCodeDosageFormTle, - 870 to stringsDe.kbvCodeDosageFormTmr, - 871 to stringsDe.kbvCodeDosageFormTon, - 872 to stringsDe.kbvCodeDosageFormTpn, - 873 to stringsDe.kbvCodeDosageFormTpo, - 874 to stringsDe.kbvCodeDosageFormTra, - 875 to stringsDe.kbvCodeDosageFormTri, - 876 to stringsDe.kbvCodeDosageFormTro, - 877 to stringsDe.kbvCodeDosageFormTrs, - 878 to stringsDe.kbvCodeDosageFormTrt, - 879 to stringsDe.kbvCodeDosageFormTsa, - 880 to stringsDe.kbvCodeDosageFormTsd, - 881 to stringsDe.kbvCodeDosageFormTse, - 882 to stringsDe.kbvCodeDosageFormTss, - 883 to stringsDe.kbvCodeDosageFormTst, - 884 to stringsDe.kbvCodeDosageFormTsy, - 885 to stringsDe.kbvCodeDosageFormTtr, - 886 to stringsDe.kbvCodeDosageFormTub, - 887 to stringsDe.kbvCodeDosageFormTue, - 888 to stringsDe.kbvCodeDosageFormTup, - 889 to stringsDe.kbvCodeDosageFormTvw, - 890 to stringsDe.kbvCodeDosageFormUta, - 891 to stringsDe.kbvCodeDosageFormVal, - 892 to stringsDe.kbvCodeDosageFormVar, - 893 to stringsDe.kbvCodeDosageFormVcr, - 894 to stringsDe.kbvCodeDosageFormVer, - 895 to stringsDe.kbvCodeDosageFormVge, - 896 to stringsDe.kbvCodeDosageFormVka, - 897 to stringsDe.kbvCodeDosageFormVli, - 898 to stringsDe.kbvCodeDosageFormVov, - 899 to stringsDe.kbvCodeDosageFormVst, - 900 to stringsDe.kbvCodeDosageFormVsu, - 901 to stringsDe.kbvCodeDosageFormVta, - 902 to stringsDe.kbvCodeDosageFormWat, - 903 to stringsDe.kbvCodeDosageFormWga, - 904 to stringsDe.kbvCodeDosageFormWka, - 905 to stringsDe.kbvCodeDosageFormWkm, - 906 to stringsDe.kbvCodeDosageFormWue, - 907 to stringsDe.kbvCodeDosageFormXdg, - 908 to stringsDe.kbvCodeDosageFormXds, - 909 to stringsDe.kbvCodeDosageFormXfe, - 910 to stringsDe.kbvCodeDosageFormXgm, - 911 to stringsDe.kbvCodeDosageFormXha, - 912 to stringsDe.kbvCodeDosageFormXhs, - 913 to stringsDe.kbvCodeDosageFormXnc, - 914 to stringsDe.kbvCodeDosageFormXpk, - 915 to stringsDe.kbvCodeDosageFormXtc, - 916 to stringsDe.kbvCodeDosageFormZam, - 917 to stringsDe.kbvCodeDosageFormZbu, - 918 to stringsDe.kbvCodeDosageFormZcr, - 919 to stringsDe.kbvCodeDosageFormZge, - 920 to stringsDe.kbvCodeDosageFormZka, - 921 to stringsDe.kbvCodeDosageFormZpa - ) -) - -public val stringsTr: Strings = Strings( - mapOf( - 0 to Singular("E-Rezept"), - 1 to Singular("Tamam"), - 3 to Singular("İptal et"), - 4 to Singular("Geri"), - 5 to Singular("Saat:"), - 6 to Singular("Saat %1\$s"), - 7 to Singular("Son güncelleme: %1\$s"), - 8 to Singular("Güncelleme başarısız. Lütfen reçetelerinizi tekrar güncelleyin."), - 9 to Singular("Dijital. Hızlı. Güvenli."), - 11 to Singular("E-Rezept uygulamasına hoş geldiniz"), - 12 to - Singular("Burada elektronik reçeteleri seçtiğiniz bir eczanede, doğrudan sitede veya çevrim içi olarak kullanabilirsiniz."), - 13 to Singular("Sağlık kartınızla daha fazla fonksiyon"), - 14 to Singular("Yeni reçetelerinizi otomatik olarak güncelleyin"), - 15 to Singular("İlaçlarınızın alınması ve dozajları hakkında bilgi"), - 16 to Singular("Eczanenizden siparişinizle ilgili bildirimler alın"), - 17 to Singular("Kullanım koşulları ve veri koruma politikası"), - 18 to - Singular("Uygulamayı kullanmak için lütfen kullanım koşullarını kabul edin ve veri koruma politikasından haberdar olduğunuzu onaylayın. Yalnızca hizmetlerin çalışması için gerekli olan veriler toplanır."), - 19 to Singular("%s\'nı okudum ve kabul ediyorum."), - 20 to Singular("Kullanım koşulları"), - 21 to Singular("Veri koruması politikası"), - 22 to Singular("Onayla"), - 23 to Singular("İleri"), - 24 to Singular("Onayla"), - 25 to Singular("Reçete ekle"), - 26 to - Singular("Bir reçete çıktısı aldınız mı? İlgili reçete kodunu tarayarak reçetleri uygulamaya ekleyebilirsiniz."), - 27 to Singular("Anlaşıldı"), - 28 to Singular("Task-ID"), - 29 to Singular("Erişim kodu"), - 30 to Singular("Kopyalandı"), - 32 to Singular("Kullanım şartları"), - 33 to Singular("Veri koruma politikası"), - 34 to Singular("Kullanım koşullarını kabul et"), - 35 to Singular("Veri koruma politikasını kabul et"), - 36 to Singular("Reçeteler"), - 37 to Singular("Reçeteler"), - 38 to Singular("Bildirimler"), - 40 to Singular("Kullan"), - 43 to Singular("Örneğin dermatolog"), - 44 to Singular("%s %s / %s %s algılandı. Daha fazla kod taransın mı?"), - 45 to Plurals(mapOf(Plurals.Type.One to "Reçete", Plurals.Type.Other to "Reçeteler")), - 46 to Plurals(mapOf(Plurals.Type.One to "Reçete", Plurals.Type.Other to "Reçeteler")), - 47 to Plurals( - mapOf( - Plurals.Type.One to "%s reçete ekle", - Plurals.Type.Other to - "%s reçete ekle" - ) - ), - 48 to Singular("Kameraya erişim reddedildi"), - 49 to - Singular("Tarayıcıyı kullanabilmek için sistem ayarlarında uygulamanın kameranıza erişmesine izin vermelisiniz."), - 50 to Singular("Kamerayı bir reçete koduna odaklayın"), - 51 to Singular("Bu geçerli bir reçete kodu değil"), - 52 to Singular("Bu reçete kodu zaten taranmış"), - 53 to Plurals( - mapOf( - Plurals.Type.One to "%s reçete algılandı", - Plurals.Type.Other to - "%s reçete algılandı" - ) - ), - 54 to Singular("İptal et"), - 55 to Singular("Kamera ışığı"), - 56 to Singular("Reçete kodlarının taranması iptal edilsin mi?"), - 57 to Singular("Taramayı iptal et"), - 58 to Singular("Devam et"), - 63 to Singular("Kart ekle"), - 64 to Singular("İşte başlıyoruz"), - 65 to Singular("Şimdi tüm fonksiyonları kullanın"), - 66 to - Singular("Uygulamanın tüm fonksiyonlarını kullanabilmek için sağlık kartınız ile giriş yapın. Bu kartı ve gerekli erişim verilerini sağlık sigortanızdan alacaksınız."), - 67 to Singular("İhtiyacınız olan şey:"), - 69 to Singular("Erişim numarası (CAN) olan bir sağlık kartı"), - 70 to Singular("Sağlık kartının PIN\'i"), - 71 to - Singular("Kart erişim numaranız (Card Access Number, kısaca: CAN) 6 hanelidir. CAN\'ı sağlık sigortası kartınızın ön yüzünün sağ üst köşesinde bulacaksınız. Burada altı haneli bir erişim numarası yoksa sağlık sigortanızdan yeni bir sağlık kartına ihtiyacınız olacaktır."), - 72 to Singular("Erişim numarasını girin"), - 73 to Singular("İstediğiniz rakamı girebilirsiniz."), - 74 to Singular("PIN\'iniz 6 ila 8 basamaklı olabilir."), - 75 to Singular("PIN girin"), - 76 to Singular("Demo modunda herhangi bir PIN girebilirsiniz."), - 77 to Singular("Tekrar dene"), - 78 to Singular("Şimdi elektronik sağlık kartınızı hazırlayın."), - 79 to - Singular("Cihazınızın sunucuya bağlanması için geçen süre, donanım ve internet hızına bağlı olarak değişebilir."), - 80 to Singular("Sunucu bağlantısı başarısız oldu."), - 81 to Singular("İnternet bağlantınızı kontrol edin ve işlemi yeniden başlatın."), - 84 to Singular("Yanlış PIN girildi."), - 85 to Plurals( - mapOf( - Plurals.Type.One to "Kartınız bloke olmadan %s denemeniz kaldı.", - Plurals.Type.Other to "Kartınız bloke olmadan %s denemeniz kaldı." - ) - ), - 86 to Singular("Yanlış CAN girildi"), - 87 to Singular("Erişim numarasını sağlık kartınızın sağ üst köşesinde bulacaksınız."), - 88 to Singular("PIN birkaç kez yanlış girildi."), - 89 to Singular("Sağlık kartınızın blokesi PUK ile açılmalıdır."), - 90 to Singular("İptal et"), - 91 to Singular("Kartı ara..."), - 92 to Singular("Sağlık kartını cihazınızın arkasına doğru tutun."), - 93 to Singular("Hala aranıyor …"), - 94 to Singular("Kartı cihazın arkasında yavaşça hareket ettirin."), - 95 to Singular("İpucu"), - 96 to Singular("Cihaz kılıfları, NFC üzerinden bağlantıyı zorlaştırabilir."), - 97 to Singular("Kart algılandı"), - 98 to Singular("Sağlık kartını hareket ettirmemeye çalışın."), - 103 to Singular("Sağlık kartı bulundu. Lütfen hareket etmeyin."), - 104 to Singular("Bağlantı koptu"), - 105 to Singular("Sağlık kartınızı tekrar cihazınızın arkasına doğru tutun."), - 106 to Singular("Başarıyla oturum açtınız"), - 107 to Singular("Not: Yalnızca son 100 güne ait reçeteler indirilir."), - 108 to Singular("Demo modu etkinleştirildi"), - 109 to - Singular("NFC özellikli bir sağlık kartınız var ve bunu demo modunda denemek ister misiniz?"), - 110 to Singular("Kart ile devam et"), - 111 to Singular("Kartsız devam et"), - 112 to Singular("Demo modu etkinleştirildi"), - 113 to Singular("Sürüm: %s"), - 114 to Singular("Build-Hash: %s"), - 115 to Singular("Hata ayıklama menüsü"), - 116 to Singular("Reçete kodu"), - 117 to Singular("Bu reçete kodunu eczanenizde tarattırın."), - 118 to Singular("Bu toplu kod %s reçete birleştirir"), - 119 to Singular("Eczanede kullan"), - 120 to Singular("Bir eczanedesiniz ve reçetenizi kullanmak istiyorsunuz."), - 121 to Singular("Sipariş ver veya rezerve et"), - 122 to - Singular("Reçetenizi bir eczaneye gönderin ve ilacınızı nasıl almak istediğinize karar verin."), - 123 to Singular("Bunun için geçerli bir sağlık kartına ihtiyacınız var."), - 124 to Singular("Eczaneyi seç"), - 125 to Singular("Örneğin Penguen eczanesi veya adresi"), - 126 to Singular("Eczaneleri kolayca bulun"), - 127 to Singular("Konumunuzu paylaşın ve bölgenizdeki eczaneleri bulun"), - 128 to Singular("Konumu paylaş"), - 129 to Singular("Şu saate kadar açık: %s"), - 130 to Singular("Tüm gün açık"), - 131 to Singular("Künye"), - 132 to Singular("Editör"), - 133 to Singular("gematik GmbH\nFriedrichstraße 136\n10117 Berlin"), - 134 to - Singular("Genel Müdür: Dr. med. Markus Leyck Dieken\n Kayıt mahkemesi: Amtsgericht Berlin-Charlottenburg\n Ticaret sicil no.: HRB 96351\n Satış vergisi kimlik numarası: DE241843684"), - 135 to Singular("İçerikten sorumlu"), - 136 to Singular("Dr. med. Markus Leyck Dieken"), - 137 to Singular("İletişim"), - 140 to Singular("Not"), - 141 to - Singular("Cinsiyet eşitliğine uygun bir dil kullanmaya çalışıyoruz. Herhangi bir hata fark ederseniz, sizden e-posta ile haber almaktan memnuniyet duyarız."), - 142 to Singular("Taranmış reçete"), - 143 to Singular("İlaç %s"), - 144 to Singular("Güncel"), - 145 to Singular("Güncelle"), - 146 to Singular("Arşiv"), - 147 to Singular("Henüz herhangi bir reçete kullanmadınız"), - 148 to Plurals(mapOf(Plurals.Type.One to "%s ilaç", Plurals.Type.Other to "%s ilaç")), - 149 to Singular("%s tarihinde kullanıldı"), - 150 to Singular("Henüz herhangi bir reçete kullanmadınız"), - 151 to Singular("Almanya\'nın modern dijital tıp platformu"), - 152 to Singular("E-posta yaz"), - 153 to Singular("Web sitesini aç"), - 154 to Singular("Hoş geldiniz"), - 155 to Singular("Oturum açmayı başlat"), - 156 to Singular("Kilidi Aç\'a basın"), - 159 to Singular("Kilidini aç"), - 162 to Singular("Oturum aç"), - 163 to Singular("İptal et"), - 164 to Singular("https://www.das-e-rezept-fuer-deutschland.de/"), - 165 to Singular("das-e-rezept-fuer-deutschland.de"), - 170 to Singular("Ayarlar"), - 171 to Singular("Adı bilinmiyor"), - 172 to Singular("Sağlık kartları"), - 173 to Singular("Kart ekle"), - 174 to Singular("Denemek için"), - 175 to - Singular("Demo modu, elektronik sağlık kartı olmadan bile uygulamanın tüm alanlarını keşfetmenize olanak tanır."), - 176 to Singular("Demo modu"), - 180 to Singular("Güvenlik"), - 181 to Singular("Sağlık bilgilerinizi yetkisiz erişime karşı koruyun."), - 185 to Singular("Yasal"), - 186 to Singular("Künye"), - 187 to Singular("Veri koruma"), - 188 to Singular("Kullanım koşulları"), - 196 to Singular("Demo modu etkinleştirildi"), - 197 to - Singular("Demo modumuz size uygulamanın tüm fonksiyonlarını gösterir - hem de sağlık kartı olmadan."), - 198 to Singular("Bir keşif turuna ne dersiniz?"), - 199 to - Singular("Demo modumuz size uygulamanın tüm fonksiyonlarını gösterir - hem de sağlık kartı olmadan."), - 200 to Singular("Demo modunu başlat"), - 201 to Singular("Güncel reçeteniz yok"), - 202 to Singular("Reçete verilerini yedekle"), - 203 to Singular("Verilerinizin parmak izi veya yüz taramasıyla daha iyi korunması."), - 204 to Singular("Şimdi aktifleştir"), - 205 to Singular("Ayrıntılar"), - 206 to Singular("Genel bakış elde edin"), - 207 to Singular("İlacınızı alır almaz bu reçeteyi kullanılmış olarak işaretleyin."), - 208 to Singular("Reçeteleri otomatik olarak güncelle"), - 209 to - Singular("Reçetelerinizin otomatik olarak kullanılmış olarak işaretlenebilmesi için oturum açın."), - 210 to Singular("Şimdi oturum aç"), - 211 to Singular("Neden sadece bu bilgileri görüyorum?"), - 212 to Singular("Sağlık bilgileriniz özel korumaya sahiptir"), - 213 to Singular("İlaç %1\$d"), - 214 to Singular("Kullanıldı olarak işaretle"), - 215 to Singular("Kullanılmadı olarak işaretle"), - 216 to Singular("Bu cihazdan sil"), - 217 to Singular("Protokol"), - 218 to Singular("Şu tarihte tarandı:"), - 219 to Singular("Saat %1\$s"), - 220 to Singular("%s tarihine kadar kullanılabilir"), - 222 to Singular("Bu ilaçla ilgili ayrıntılar"), - 223 to Singular("İlaç türü"), - 224 to Singular("Paket boyutu"), - 225 to Singular("İlaç merkezi numarası (PZN)"), - 226 to Singular("Kullanım talimatları"), - 227 to - Singular("Lütfen ilaç planınızdaki alım talimatlarına veya doktorunuzun yazılı dozaj talimatlarına uyun."), - 228 to Singular("Sigortalı kişi"), - 229 to Singular("Ad"), - 230 to Singular("Adres"), - 231 to Singular("Doğum tarihi"), - 232 to Singular("Sağlık sigortası / ödeyenler"), - 233 to Singular("Durum"), - 234 to Singular("Sigorta numarası"), - 235 to Singular("Reçete yazan kişi"), - 236 to Singular("Ad"), - 237 to Singular("Uzman doktor"), - 238 to Singular("Doktor numarası (LANR)"), - 239 to Singular("Kurum"), - 240 to Singular("Ad"), - 241 to Singular("Adres"), - 242 to Singular("Kuruluş numarası"), - 243 to Singular("Telefon numarası"), - 244 to Singular("E-posta"), - 245 to Singular("İş kazası"), - 246 to Singular("Kaza günü"), - 247 to Singular("Kaza şirketi veya işveren numarası"), - 248 to Singular("Bu reçeteyi kalıcı olarak silmek ister misiniz?"), - 249 to Singular("Sil"), - 250 to Singular("İptal et"), - 254 to Singular("Bu acil bir durum"), - 255 to Singular("Bu ilaç, acil servis ücreti olmadan geceleri eczanede de kullanılabilir."), - 256 to Singular("Muadil mümkün"), - 257 to - Singular("Muadillere izin verilir. Sağlık sigortanızın yasal gereklilikleri nedeniyle size bir alternatif verilebilir."), - 258 to Singular("Bağlayıcı bir rezerve et"), - 259 to Singular("Kurye hizmeti talep edin"), - 260 to Singular("Kargo ile teslim ettir"), - 262 to - Singular("Reçeteli ilaçlar için ek ödemelerin de geçerli olabileceğini lütfen unutmayın."), - 263 to Singular("Açılış saatleri"), - 264 to Singular("Web sitesi"), - 265 to Singular("Rezervasyon"), - 266 to Singular("Bu reçeteleri %s eczanesinde bağlayıcı olarak kullanmak istiyor musunuz?"), - 267 to Singular("Reçeteler"), - 268 to Singular("Kullan"), - 269 to Singular("Kurye hizmeti"), - 270 to Singular("Teslimat adresi"), - 271 to Singular("Nasıl yardımcı olabiliriz?"), - 272 to Singular("Teslimat adresi mi değişti? Eczaneye başka bir mesajınız var mı?"), - 273 to Singular("Şimdi ara"), - 274 to Singular("Kargo"), - 275 to Singular("Protokol"), - 276 to Singular("Ayarlanmış eylem olmadan"), - 278 to Singular("Sadece bugün geçerli"), - 280 to Singular("Reçete bloğunu yeniden adlandır"), - 281 to Singular("Bu reçete bloğu için bir ad atayabilirsiniz."), - 282 to Singular("Oturum aç"), - 283 to Singular("Oturum aç"), - 284 to Singular("En az Android 7\'ye sahip NFC özellikli bir akıllı telefon"), - 285 to Singular("NFC\'yi etkinleştir"), - 286 to - Singular("Sağlık kartınız ile oturum açmak için lütfen cihazınızın NFC fonksiyonunu etkinleştirin."), - 287 to Singular("Etkinleştir"), - 288 to Singular("Yeni bir sağlık kartını nasıl alabilirim?"), - 289 to Singular("Sağlık sigortanız bu konuda size yardımcı olur."), - 290 to Singular("Nasıl PIN alabilirim?"), - 291 to - Singular("Sağlık sigorta şirketinizden sağlık kartınızın PIN\'i için ayrı bir mektup alacaksınız."), - 292 to Singular("Erişim verilerinizi gelecekteki oturum açmalar için kaydetmek ister misiniz?"), - 293 to Singular("Erişim verilerini kaydet"), - 294 to - Singular("Kullanışlı ve yakında sizin için kullanılabilir: Bu amaçla verileriniz cihazda biyometrik olarak korunacaktır"), - 295 to Singular("Yedekleme mümkün değil"), - 296 to Singular("Güvenli sensör yok veya biyometrik yedekleme kurulmadı."), - 297 to Singular("Erişim verilerini kaydetme"), - 298 to - Singular("Veri tasarrufu: Uygulamayı her başlattığınızda erişim verilerinizi girmenizi gerektirir"), - 299 to Singular("Tekrarla"), - 300 to Singular("Ana sayfaya git"), - 301 to Singular("Şu tarihte kullanıldı olarak işaretlendi:"), - 302 to Singular("Şu tarihte kullanılmadı olarak işaretlendi:"), - 303 to Singular("Tekli kodlar olarak göster"), - 304 to Singular("Toplu kod olarak göster"), - 305 to Singular("%s / %s"), - 306 to Singular("Reçete kullanıldı mı?"), - 307 to Singular("Bu reçeteyi kullanılmış olarak işaretlemek ister misiniz?"), - 308 to Singular("Kullanılmadı"), - 309 to Singular("Kullanıldı"), - 310 to Singular("Şu saatte açılıyor: %s"), - 311 to Singular("+49 800 277 377 7"), - 312 to Singular("Teknik destek hattı"), - 313 to Singular("Reçeteler için tarayıcıyı açın"), - 314 to Singular("Ayarlar"), - 315 to Singular("+49 800 277 377 7"), - 316 to Singular("Lütfen parmak izi veya yüz tanıma yoluyla kendinizi tanıtın."), - 317 to Singular("Not"), - 318 to - Singular("Bu değişiklik, yalnızca uygulamayı yeniden başlattıktan sonra geçerli olacaktır."), - 319 to Singular("Tamam"), - 320 to Singular("İzleme"), - 321 to - Singular("Bu uygulamayı daha iyi hale getirmemize yardımcı olun. Tüm kullanım verileri anonim olarak toplanır ve yalnızca kullanıcı deneyimini iyileştirmek için hizmet verir."), - 322 to Singular("İzlemeye izin ver"), - 323 to - Singular("Uygulamada bir çökme veya hata olması durumunda uygulama bize nedenleri hakkında bilgi gönderir. Ayrıca işletim sistemi sürümü ve kullanılan donanımlar ile ilgili bilgiler de gönderilir."), - 324 to Singular("Ekran görüntülerini gizle"), - 325 to - Singular("Uygulamaları değiştirdiğinizde önizleme görüntüsünün görüntülenmesini engeller"), - 326 to - Singular("E-Rezept\'in kullanıcı davranışınızı anonim olarak analiz etmesine izin veriyor musunuz?"), - 327 to - Singular("Bu, telefonunuzun donanım ve yazılım bilgilerini, E-Rezept uygulamasının ayarlarını ve kullanım kapsamını içerir, ancak asla kişiliğiniz veya sağlığınızla ilgili verileri içermez.\nVeriler, veri işleyenler tarafından sadece gematik GmbH\'ye sunulur ve en geç 180 gün sonra silinir. Analizi istediğiniz zaman uygulama menüsünden devre dışı bırakabilirsiniz.\nBu veriler, hangi fonksiyonların sıklıkla kullanıldığını anlamamızı ve bunları geliştirmemizi sağlar. Ayrıca, daha eski teknolojinin ne kadar süreyle desteklenmesi gerektiğini ve örneğin ne zaman (çok fazla) kullanıcıyı etkilemeden daha yeni bir işletim sistemi sürümünü zorunlu hale getirmemiz gerektiğini tahmin edebiliriz."), - 328 to Singular("İzin ver"), - 340 to Plurals( - mapOf( - Plurals.Type.One to "Sizin için %s ilaç reçetelendirildi", - Plurals.Type.Other to "Sizin için %s ilaç reçetelendirildi" - ) - ), - 341 to Singular("Eczanede kullanmak için buraya dokunun"), - 342 to Singular("Şimdi kullan"), - 343 to Singular("Tümünü göster"), - 344 to Singular("Düzenlemeyi sil"), - 345 to Singular("Kullanıldı olarak işaretlendi"), - 346 to Singular("Geri al"), - 347 to Singular("Daha fazla göster"), - 348 to Singular("Daha az göster"), - 349 to Singular("Teknik bilgiler"), - 350 to Singular("Oturumu kapat"), - 351 to Singular("Sağlık ağına tüm erişim verileri silinecektir. Reçete verileriniz korunur."), - 352 to Singular("Bu, erişim verilerinizi siler."), - 353 to Singular("Oturumu kapat"), - 354 to Singular("İptal et"), - 355 to Singular("Uygulamadan çıkmak ister misiniz?"), - 356 to Singular("Reçete verilerinizin güvenliği"), - 357 to - Singular("Lütfen bu cihazı paylaşabileceğiniz ve biyometrik özellikleri bu cihazda saklanabilecek veya cihaz PIN\'ini, kaydırma hareketini veya şifreyi bilen kişilerin de reçetelerinize erişebileceğini unutmayın."), - 358 to Singular("Bağlayıcı olarak kullan?"), - 359 to - Singular("Bu vesileyle reçeteleriniz bu eczaneye gönderilecektir. Daha sonra bunları artık başka bir eczanede kullanamazsınız."), - 360 to Singular("İptal et"), - 361 to Singular("Şimdi kullan"), - 362 to Singular("Başarıyla kullanıldı"), - 363 to - Singular("Eczane teslimat ayrıntılarını netleştirmek için en kısa sürede sizinle iletişime geçecektir."), - 364 to Singular("Siparişinizi tarayıcıda tamamlayın"), - 365 to Singular("Ana sayfaya geç"), - 366 to - Singular("Çevrim içi eczanesi ilaçlarınızla birlikte bir alışveriş sepeti oluşturacaktır. Bu işlem birkaç dakika sürebilir."), - 367 to - Singular("\"Alışveriş sepetini aç\"a dokunun ve eczanenin web sitesinde siparişinizi tamamlayın."), - 368 to Singular("Ana sayfaya git"), - 369 to Singular("Gönderim başarısız oldu"), - 370 to Singular("Tekrarla"), - 371 to - Singular("Siparişiniz genellikle kısa sürede teslim almanız için hazırdır. Kesin randevu için lütfen eczane ile irtibata geçin."), - 372 to Singular("Alışveriş sepetiniz hazır"), - 373 to Singular("Teslim alma kodu al"), - 374 to Singular("Bildirim alındı"), - 375 to Singular("Teslim alma kodunu göster"), - 376 to Singular("Alışveriş sepetini aç"), - 377 to Singular("Bu kodu eczanenizde gösterin."), - 378 to Singular("Teslim alma kodu"), - 379 to Singular("Bildirim yok"), - 380 to Singular("Henüz herhangi bir bildirim almadınız"), - 381 to Singular("Maalesef eczanenizden gelen mesaj boştu. Lütfen eczanenizle iletişime geçin."), - 382 to Singular("E-posta programı kurulmamış"), - 383 to Singular("Sonuç yok"), - 384 to Singular("Bu arama terimi ile herhangi bir sonuç bulamadık."), - 388 to Singular("Open Source Lisansları"), - 389 to Singular("İletişim"), - 390 to Singular("Teknik destek hattını ara"), - 391 to Singular("E-posta yaz"), - 392 to Singular("Ankete katıl"), - 394 to Singular("+49 800 277 377 7"), - 2 to stringsDe.understand, - 10 to stringsDe.onBoardingPage1Headline, - 31 to stringsDe.consistentPassword, - 39 to stringsDe.presBottombarPharmacies, - 41 to stringsDe.prescriptionItemExpirationDaysNew, - 42 to stringsDe.prescriptionItemAcceptDays, - 59 to stringsDe.camAcceptMlkitTitle, - 60 to stringsDe.camAcceptMlkitBody, - 61 to stringsDe.camAcceptMlkitAccept, - 62 to stringsDe.camAcceptMlkitDecline, - 68 to stringsDe.cdwIntroWhatYouNeedNoEgk, - 82 to stringsDe.cdwNfcErrorTitleInvalidCertificate, - 83 to stringsDe.cdwNfcErrorBodyInvalidCertificate, - 99 to stringsDe.cdwNfcCommunicationHeadlineTrustedChannelEstablished, - 100 to stringsDe.cdwNfcCommunicationHeadlineCertificateLoaded, - 101 to stringsDe.cdwNfcCommunicationHeadlinePinVerified, - 102 to stringsDe.cdwNfcCommunicationHeadlineChallengeSigned, - 138 to stringsDe.menuLegalNoticeUrl, - 139 to stringsDe.legalNoticeEmail, - 157 to stringsDe.authSubtitleError, - 158 to stringsDe.authInfoError, - 160 to stringsDe.authMoreHotline, - 161 to stringsDe.authMoreWeb, - 166 to stringsDe.authPromptEnterPassword, - 167 to stringsDe.authPromptCheckPassword, - 168 to stringsDe.authErrorFailedAuthsHeadline, - 169 to stringsDe.authErrorFailedAuthsInfo, - 177 to stringsDe.settingsAccessibilityHeadline, - 178 to stringsDe.settingsAccessibilityZoomToggle, - 179 to stringsDe.settingsAccessibilityZoomInfo, - 182 to stringsDe.settingsAppprotectionDeviceSecurityHeader, - 183 to stringsDe.settingsAppprotectionDeviceSecurityInfo, - 184 to stringsDe.settingsAppprotectionDeviceSecurityDisabledInfo, - 189 to stringsDe.tokenHeadline, - 190 to stringsDe.accessTokenTitle, - 191 to stringsDe.singleSignOnTokenTitle, - 192 to stringsDe.noAccessToken, - 193 to stringsDe.noSingleSignOnToken, - 194 to stringsDe.copied, - 195 to stringsDe.copyContentDescription, - 221 to stringsDe.presDetailMedicationExpiryUntil, - 251 to stringsDe.presDetailUnRedeemMsg, - 252 to stringsDe.presDetailUnRedeemAll, - 253 to stringsDe.presDetailUnRedeemSelected, - 261 to stringsDe.pharmDetailHintHeader, - 277 to stringsDe.prescriptionItemAcceptOnlyToday, - 279 to stringsDe.prescriptionItemExpired, - 329 to stringsDe.settingsDeviceSecurityAllow, - 330 to stringsDe.settingsAppprotectionModePasswordHeadline, - 331 to stringsDe.settingsAppprotectionModePasswordInfo, - 332 to stringsDe.settingsPasswordHeadline, - 333 to stringsDe.settingsPasswordSave, - 334 to stringsDe.settingsPasswordAccShowPasswordToggle, - 335 to stringsDe.settingsPasswordEnterPassword, - 336 to stringsDe.settingsPasswordHint, - 337 to stringsDe.settingsPasswordRepeatPassword, - 338 to stringsDe.settingsPasswordStrength, - 339 to stringsDe.settingsPasswordSuggestions, - 385 to stringsDe.searchPharmacyErrorTitle, - 386 to stringsDe.searchPharmacyErrorSubtitle, - 387 to stringsDe.searchPharmacyErrorAction, - 393 to stringsDe.settingsContactFeedbackForm, - 395 to stringsDe.settingsContactMailAddress, - 396 to stringsDe.settingsContactFeedbackAdress, - 397 to stringsDe.settingsShowToken, - 398 to stringsDe.learnMoreBtn, - 399 to stringsDe.onBoardingPage1AccImage, - 400 to stringsDe.onBoardingPage2AccImage, - 401 to stringsDe.onBoardingPage3AccImage, - 402 to stringsDe.onBoardingPage5Header, - 403 to stringsDe.onBoardingPage5SubHeader, - 404 to stringsDe.onBoardingPage5Info1, - 405 to stringsDe.onBoardingPage5Info2, - 406 to stringsDe.onBoardingPage5Info3, - 407 to stringsDe.onBoardingPage5Label, - 408 to stringsDe.onBoardingPage5LabelInfo, - 409 to stringsDe.onBoardingPage5Anonym, - 410 to stringsDe.onBoardingPage5Next, - 411 to stringsDe.onBoardingSecureAppPageHeader, - 412 to stringsDe.onBoardingSecureAppPageInfo, - 413 to stringsDe.onboardingSecureAppOr, - 414 to stringsDe.onboardingSecureAppButtonBest, - 415 to stringsDe.onboardingSecureAppButtonBestChosen, - 416 to stringsDe.onboardingSecureAppButtonBestInfo, - 417 to stringsDe.settingsTrackingDialogText1, - 418 to stringsDe.settingsTrackingDialogText2, - 419 to stringsDe.settingsTrackingDialogText3, - 420 to stringsDe.settingsTrackingAllowTitle, - 421 to stringsDe.settingsTrackingNotAllow, - 422 to stringsDe.settingsTrackingDisallowInfo, - 423 to stringsDe.settingsTrackingAllowInfo, - 424 to stringsDe.settingsTrackingAllowEmoji, - 425 to stringsDe.settingsFeedbackFormHeader, - 426 to stringsDe.settingsFeedbackFormPlaceholder, - 427 to stringsDe.seetingsFeedbackFormAdditionalDataInfo, - 428 to stringsDe.seetingsFeedbackFormAdditionalDataOs, - 429 to stringsDe.seetingsFeedbackFormAdditionalDataOsDetail, - 430 to stringsDe.seetingsFeedbackFormAdditionalDataDevice, - 431 to stringsDe.seetingsFeedbackFormAdditionalDataDeviceDetail, - 432 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmode, - 433 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmodeOn, - 434 to stringsDe.seetingsFeedbackFormAdditionalDataDarkmodeOff, - 435 to stringsDe.seetingsFeedbackFormAdditionalDataLanguage, - 436 to stringsDe.settingsFeedbackFormSend, - 437 to stringsDe.settingsFeedbackFormHeadline, - 438 to stringsDe.presDetailMedicationRedeemButtonText, - 439 to stringsDe.redeemSyncedPrescriptionsDialogHeader, - 440 to stringsDe.redeemSyncedPrescriptionsDialogInfo, - 441 to stringsDe.redeemSyncedPrescriptionsDialogOk, - 442 to stringsDe.logoutDeleteNoAccess, - 443 to stringsDe.communicationErrorActionText, - 444 to stringsDe.communicationErrorInboxHeader, - 445 to stringsDe.communicationErrorInboxDisplayText, - 446 to stringsDe.messagesContactMailAddress, - 447 to stringsDe.messagesContactEmailSubject, - 448 to stringsDe.messagesContactEmailBody, - 449 to stringsDe.messagesContactEmailDataTransparency, - 450 to stringsDe.messagesContactEmailErrorCode, - 451 to stringsDe.insecureDeviceTitle, - 452 to stringsDe.insecureDeviceHeader, - 453 to stringsDe.insecureDeviceInfo, - 454 to stringsDe.insecureDeviceAccept, - 455 to stringsDe.alternateAuthHeader, - 456 to stringsDe.alternateAuthInfo, - 457 to stringsDe.presDetailMedicationRedeemedOn, - 458 to stringsDe.presDetailSubstitutedHeader, - 459 to stringsDe.presDetailSubstitutedInfo, - 460 to stringsDe.pharmacyDetailDataInfo, - 461 to stringsDe.pharmacyDetailDataInfoDomain, - 462 to stringsDe.pharmacyDetailDataInfoBtn, - 463 to stringsDe.pharmacyDetailPharmacyPortalUri, - 464 to stringsDe.pharmacyDetailDataInfoFaqsUri, - 465 to stringsDe.pharmacyDetailNotReadyHeader, - 466 to stringsDe.pharmacyDetailNotReadyInfo, - 467 to stringsDe.searchPharmacyReadyFlag, - 468 to stringsDe.searchPharmaciesFilterReady, - 469 to stringsDe.searchPharmaciesFilterOpenNow, - 470 to stringsDe.searchPharmaciesFilterDeliveryService, - 471 to stringsDe.searchPharmaciesFilterOnlineService, - 472 to stringsDe.searchPharmaciesFilterHeader, - 473 to stringsDe.searchPharmaciesFilterSectionFavorites, - 474 to stringsDe.searchPharmaciesFilter, - 475 to stringsDe.searchPharmaciesLocationNaInfo, - 476 to stringsDe.searchPharmaciesLocationNaHeader, - 477 to stringsDe.errorMessageNetworkNotAvailable, - 478 to stringsDe.errorMessageServerCommunicationFailed, - 479 to stringsDe.errorMessageVauError, - 480 to stringsDe.settingsNoActiveToken, - 481 to stringsDe.prescriptionDetailRedeemed, - 482 to stringsDe.prescriptionDetailUnRedeemed, - 483 to stringsDe.insecureDeviceTitleSafetynet, - 484 to stringsDe.insecureDeviceHeaderSafetynet, - 485 to stringsDe.insecureDeviceInfoSafetynet, - 486 to stringsDe.insecureDeviceAcceptSafetynet, - 487 to stringsDe.insecureDeviceSafetynetMoreInfo, - 488 to stringsDe.insecureDeviceSafetynetLinkText, - 489 to stringsDe.insecureDeviceSafetynetLink, - 490 to stringsDe.cdwHealthCardInfoTitle, - 491 to stringsDe.cdwHealthcardInfoHeadline, - 492 to stringsDe.cdwHealthcardInfo, - 493 to stringsDe.cdwHealthcardInfoBullet, - 494 to stringsDe.cdwHealthcardInfoCan, - 495 to stringsDe.cdwHealthcardInfoCard, - 496 to stringsDe.cdwHealthcardInfoPin, - 497 to stringsDe.cdwHealthcardInfoPinPin, - 498 to stringsDe.cdwHealthCardInfoHintDescription, - 499 to stringsDe.cdwHealthcardInfoMailDescription, - 500 to stringsDe.cdwHealthcardInfoMail, - 501 to stringsDe.cdwRegisterWithHealthyCard, - 502 to stringsDe.cdwRegisterWithHealthInsurance, - 503 to stringsDe.cdwRegisterHealtyCardInfo, - 504 to stringsDe.cdwRegisterHealthInsuranceInfo, - 505 to stringsDe.cdwRegisterTitle, - 506 to stringsDe.cdwRegisterBody, - 507 to stringsDe.cdwRegisterScreenTitle, - 508 to stringsDe.cdwManRegisterAccessibility, - 509 to stringsDe.cdwWomanRegisterAccessibility, - 510 to stringsDe.cdwNoNfc, - 511 to stringsDe.profileEditName, - 512 to stringsDe.profileEditNameInfo, - 513 to stringsDe.profileEditNamePlaceHolder, - 514 to stringsDe.settingsProfileEditName, - 515 to stringsDe.settingsProfileDelete, - 516 to stringsDe.settingsProfilesHeadline, - 517 to stringsDe.onboardingProfileHeader, - 518 to stringsDe.onboardingProfileInfo, - 519 to stringsDe.onboardingProfileInputName, - 520 to stringsDe.settingsAddProfilesHintTitle, - 521 to stringsDe.settingsAddProfilesHintInfo, - 522 to stringsDe.settingsAddProfile, - 523 to stringsDe.profileSetupSave, - 524 to stringsDe.cdwHealthInsurancePageTitle, - 525 to stringsDe.cdwHealthInsuranceTitle, - 526 to stringsDe.cdwHealthInsuranceBodyWhatYouNeed, - 527 to stringsDe.cdwHealthInsuranceBodyHowToGet, - 528 to stringsDe.cdwHealthInsuranceCaptionRecognizeHealthcard, - 529 to stringsDe.cdwHealthInsuranceLearnMore, - 530 to stringsDe.cdwHealthInsuranceSelectCompany, - 531 to stringsDe.cdwHealthInsuranceNoCompanySelected, - 532 to stringsDe.cdwHealthInsuranceWhatToDo, - 533 to stringsDe.cdwHealthInsuranceNoCantactsTitle, - 534 to stringsDe.cdwHealthInsuranceNoCantactsBody, - 535 to stringsDe.cdwHealthInsuranceContactHealthcardPin, - 536 to stringsDe.cdwHealthInsuranceContactPinOnly, - 537 to stringsDe.cdwHealthInsuranceContactInsuranceCompany, - 538 to stringsDe.cdwHealthInsuranceMailSubject, - 539 to stringsDe.settingsHealthInsuranceContactTitle, - 540 to stringsDe.settingsHealthInsuranceContactBody, - 541 to stringsDe.settingsHealthInsuranceContactAction, - 542 to stringsDe.cdwCapabilityTitle, - 543 to stringsDe.cdwCapabilityHeadline, - 544 to stringsDe.cdwCapabilityBody, - 545 to stringsDe.cdwCapabilityMore, - 546 to stringsDe.editProfileEmptyProfileName, - 547 to stringsDe.editProfileDuplicatedProfileName, - 548 to stringsDe.editProfileTitle, - 549 to stringsDe.editProfileColorSelected, - 550 to stringsDe.editProfileBackgroundColor, - 551 to stringsDe.profileColorNameGray, - 552 to stringsDe.profileColorSunDew, - 553 to stringsDe.profileColorNamePink, - 554 to stringsDe.profileColorNameTree, - 555 to stringsDe.profileColorNameMoon, - 556 to stringsDe.settingsProfileNotConnected, - 557 to stringsDe.settingsProfileConnected, - 558 to stringsDe.settingsProfileLastAuthenticatedOn, - 559 to stringsDe.removeProfileHeader, - 560 to stringsDe.removeProfileDetailMessage, - 561 to stringsDe.removeProfileYes, - 562 to stringsDe.removeProfileNo, - 563 to stringsDe.removeProfile, - 564 to stringsDe.profileEditNameForDefault, - 565 to stringsDe.profileEditNameForDefaultInfo, - 566 to stringsDe.cdwNfcErrorTitleInvalidOcspResponseOfHealthCardCertificate, - 567 to stringsDe.cdwNfcErrorBodyInvalidOcspResponseOfHealthCardCertificate, - 568 to stringsDe.loginDescription, - 569 to stringsDe.presDetailHealthPortalDescription, - 570 to stringsDe.presDetailHealthPortalDescriptionUrlInfo, - 571 to stringsDe.presDetailHealthPortalDescriptionUrl, - 572 to stringsDe.selectProfile, - 573 to stringsDe.editProfiles, - 574 to stringsDe.zeroPrescriptionsUpdatet, - 575 to stringsDe.prescriptionsUpdated, - 576 to stringsDe.prescriptionStatusReady, - 577 to stringsDe.prescriptionStatusInProgress, - 578 to stringsDe.prescriptionStatusCompleted, - 579 to stringsDe.prescriptionStatusUnknown, - 580 to stringsDe.pharmacyDetailTitle, - 581 to stringsDe.settingsShowAuditEvents, - 582 to stringsDe.settingsShowAuditEventsInfo, - 583 to stringsDe.settingsShowTokenInfo, - 584 to stringsDe.autiteventsHeadline, - 585 to stringsDe.logoutProfile, - 586 to stringsDe.loginProfile, - 587 to stringsDe.functionNotAvaillableOnDemoMode, - 588 to stringsDe.presDetailMedicationInProgress, - 589 to stringsDe.directAssignmentWillBeForwardet, - 590 to stringsDe.noAuditEventsHeader, - 591 to stringsDe.noAuditEventsNotAuthenticatedInfo, - 592 to stringsDe.noAuditEventsEmptyProtocolListInfo, - 593 to stringsDe.auditEventsUpdatedAt, - 594 to stringsDe.logoutDeleteInProgress, - 595 to stringsDe.profileNotConnected, - 596 to stringsDe.profileConnected, - 597 to stringsDe.connectProfileHeader, - 598 to stringsDe.connectProfileInfo, - 599 to stringsDe.connectProfileConnect, - 600 to stringsDe.noTokenAvailableInDemoMode, - 601 to stringsDe.settingsAddProfileNotAllowed, - 602 to stringsDe.desktopMenuInfo, - 603 to stringsDe.desktopMenuTerms, - 604 to stringsDe.desktopMenuData, - 605 to stringsDe.desktopMainWelcomeTitle, - 606 to stringsDe.desktopMainWelcomeSubtitle, - 607 to stringsDe.desktopMainLoginWithHealthcard, - 608 to stringsDe.desktopMainPrescriptions, - 609 to stringsDe.desktopMainCommunications, - 610 to stringsDe.desktopMainRedeemablePrescriptions, - 611 to stringsDe.desktopMainRedeemedPrescriptions, - 612 to stringsDe.desktopMainPharmacyCommunications, - 613 to stringsDe.desktopMainPharmacyProtocol, - 614 to stringsDe.desktopMainRefresh, - 615 to stringsDe.desktopMainZoomIn, - 616 to stringsDe.desktopMainZoomOut, - 617 to stringsDe.desktopMainHelp, - 618 to stringsDe.desktopMainLogout, - 619 to stringsDe.desktopPrescriptionNoData, - 620 to stringsDe.desktopLoginPageDataTerms, - 621 to stringsDe.desktopLoginPageConnectReader, - 622 to stringsDe.desktopLoginPageEnterCan, - 623 to stringsDe.desktopLoginPageEnterPin, - 624 to stringsDe.desktopLoginPageConnectHealthcard, - 625 to stringsDe.desktopLoginPageReaderSearchHealthcard, - 626 to stringsDe.desktopLoginPageReaderFoundHealthcard, - 627 to stringsDe.desktopLoginPageReaderError, - 628 to stringsDe.desktopLoginPageConnectHealthcardErrorIoTitle, - 629 to stringsDe.desktopLoginPageConnectHealthcardErrorIoSubtitle, - 630 to stringsDe.desktopPrescriptionExpiresOn, - 631 to stringsDe.desktopPrescriptionAcceptUntil, - 632 to stringsDe.desktopPrescriptionExpired, - 633 to stringsDe.desktopPrescriptionPrescribedOn, - 634 to stringsDe.desktopPrescriptionShowDmc, - 635 to stringsDe.desktopCommunicationOnPremise, - 636 to stringsDe.desktopCommunicationDelivery, - 637 to stringsDe.desktopCommunicationShipment, - 638 to stringsDe.desktopTermsLink, - 639 to stringsDe.desktopDataLink, - 640 to stringsDe.desktopHelpLink, - 641 to stringsDe.desktopContactLink, - 642 to stringsDe.kbvMemberStatus1, - 643 to stringsDe.kbvMemberStatus3, - 644 to stringsDe.kbvMemberStatus5, - 645 to stringsDe.kbvNormSizeKa, - 646 to stringsDe.kbvNormSizeKtp, - 647 to stringsDe.kbvNormSizeN1, - 648 to stringsDe.kbvNormSizeN2, - 649 to stringsDe.kbvNormSizeN3, - 650 to stringsDe.kbvNormSizeNb, - 651 to stringsDe.kbvNormSizeSonstiges, - 652 to stringsDe.kbvCodeDosageFormAeo, - 653 to stringsDe.kbvCodeDosageFormAmp, - 654 to stringsDe.kbvCodeDosageFormApa, - 655 to stringsDe.kbvCodeDosageFormAsn, - 656 to stringsDe.kbvCodeDosageFormAso, - 657 to stringsDe.kbvCodeDosageFormAto, - 658 to stringsDe.kbvCodeDosageFormAtr, - 659 to stringsDe.kbvCodeDosageFormAub, - 660 to stringsDe.kbvCodeDosageFormAuc, - 661 to stringsDe.kbvCodeDosageFormAug, - 662 to stringsDe.kbvCodeDosageFormAus, - 663 to stringsDe.kbvCodeDosageFormBad, - 664 to stringsDe.kbvCodeDosageFormBal, - 665 to stringsDe.kbvCodeDosageFormBan, - 666 to stringsDe.kbvCodeDosageFormBeu, - 667 to stringsDe.kbvCodeDosageFormBin, - 668 to stringsDe.kbvCodeDosageFormBon, - 669 to stringsDe.kbvCodeDosageFormBpl, - 670 to stringsDe.kbvCodeDosageFormBre, - 671 to stringsDe.kbvCodeDosageFormBta, - 672 to stringsDe.kbvCodeDosageFormCre, - 673 to stringsDe.kbvCodeDosageFormDfl, - 674 to stringsDe.kbvCodeDosageFormDil, - 675 to stringsDe.kbvCodeDosageFormDis, - 676 to stringsDe.kbvCodeDosageFormDka, - 677 to stringsDe.kbvCodeDosageFormDos, - 678 to stringsDe.kbvCodeDosageFormDra, - 679 to stringsDe.kbvCodeDosageFormDrm, - 680 to stringsDe.kbvCodeDosageFormDsc, - 681 to stringsDe.kbvCodeDosageFormDss, - 682 to stringsDe.kbvCodeDosageFormEdp, - 683 to stringsDe.kbvCodeDosageFormEin, - 684 to stringsDe.kbvCodeDosageFormEle, - 685 to stringsDe.kbvCodeDosageFormEli, - 686 to stringsDe.kbvCodeDosageFormEmu, - 687 to stringsDe.kbvCodeDosageFormEss, - 688 to stringsDe.kbvCodeDosageFormEsu, - 689 to stringsDe.kbvCodeDosageFormExt, - 690 to stringsDe.kbvCodeDosageFormFbe, - 691 to stringsDe.kbvCodeDosageFormFbw, - 692 to stringsDe.kbvCodeDosageFormFda, - 693 to stringsDe.kbvCodeDosageFormFer, - 694 to stringsDe.kbvCodeDosageFormFet, - 695 to stringsDe.kbvCodeDosageFormFla, - 696 to stringsDe.kbvCodeDosageFormFle, - 697 to stringsDe.kbvCodeDosageFormFlu, - 698 to stringsDe.kbvCodeDosageFormFmr, - 699 to stringsDe.kbvCodeDosageFormFol, - 700 to stringsDe.kbvCodeDosageFormFrb, - 701 to stringsDe.kbvCodeDosageFormFse, - 702 to stringsDe.kbvCodeDosageFormFta, - 703 to stringsDe.kbvCodeDosageFormGek, - 704 to stringsDe.kbvCodeDosageFormGel, - 705 to stringsDe.kbvCodeDosageFormGli, - 706 to stringsDe.kbvCodeDosageFormGlo, - 707 to stringsDe.kbvCodeDosageFormGmr, - 708 to stringsDe.kbvCodeDosageFormGpa, - 709 to stringsDe.kbvCodeDosageFormGra, - 710 to stringsDe.kbvCodeDosageFormGse, - 711 to stringsDe.kbvCodeDosageFormGul, - 712 to stringsDe.kbvCodeDosageFormHas, - 713 to stringsDe.kbvCodeDosageFormHkm, - 714 to stringsDe.kbvCodeDosageFormHkp, - 715 to stringsDe.kbvCodeDosageFormHpi, - 716 to stringsDe.kbvCodeDosageFormHvw, - 717 to stringsDe.kbvCodeDosageFormIfa, - 718 to stringsDe.kbvCodeDosageFormIfb, - 719 to stringsDe.kbvCodeDosageFormIfd, - 720 to stringsDe.kbvCodeDosageFormIfe, - 721 to stringsDe.kbvCodeDosageFormIff, - 722 to stringsDe.kbvCodeDosageFormIfk, - 723 to stringsDe.kbvCodeDosageFormIfl, - 724 to stringsDe.kbvCodeDosageFormIfs, - 725 to stringsDe.kbvCodeDosageFormIha, - 726 to stringsDe.kbvCodeDosageFormIhp, - 727 to stringsDe.kbvCodeDosageFormIie, - 728 to stringsDe.kbvCodeDosageFormIil, - 729 to stringsDe.kbvCodeDosageFormIim, - 730 to stringsDe.kbvCodeDosageFormIka, - 731 to stringsDe.kbvCodeDosageFormIlo, - 732 to stringsDe.kbvCodeDosageFormImp, - 733 to stringsDe.kbvCodeDosageFormInf, - 734 to stringsDe.kbvCodeDosageFormInh, - 735 to stringsDe.kbvCodeDosageFormIni, - 736 to stringsDe.kbvCodeDosageFormInl, - 737 to stringsDe.kbvCodeDosageFormIns, - 738 to stringsDe.kbvCodeDosageFormIst, - 739 to stringsDe.kbvCodeDosageFormIsu, - 740 to stringsDe.kbvCodeDosageFormIup, - 741 to stringsDe.kbvCodeDosageFormKan, - 742 to stringsDe.kbvCodeDosageFormKap, - 743 to stringsDe.kbvCodeDosageFormKat, - 744 to stringsDe.kbvCodeDosageFormKda, - 745 to stringsDe.kbvCodeDosageFormKeg, - 746 to stringsDe.kbvCodeDosageFormKer, - 747 to stringsDe.kbvCodeDosageFormKgu, - 748 to stringsDe.kbvCodeDosageFormKid, - 749 to stringsDe.kbvCodeDosageFormKii, - 750 to stringsDe.kbvCodeDosageFormKks, - 751 to stringsDe.kbvCodeDosageFormKli, - 752 to stringsDe.kbvCodeDosageFormKlt, - 753 to stringsDe.kbvCodeDosageFormKmp, - 754 to stringsDe.kbvCodeDosageFormKmr, - 755 to stringsDe.kbvCodeDosageFormKod, - 756 to stringsDe.kbvCodeDosageFormKom, - 757 to stringsDe.kbvCodeDosageFormKon, - 758 to stringsDe.kbvCodeDosageFormKpg, - 759 to stringsDe.kbvCodeDosageFormKri, - 760 to stringsDe.kbvCodeDosageFormKss, - 761 to stringsDe.kbvCodeDosageFormKsu, - 762 to stringsDe.kbvCodeDosageFormKta, - 763 to stringsDe.kbvCodeDosageFormLan, - 764 to stringsDe.kbvCodeDosageFormLii, - 765 to stringsDe.kbvCodeDosageFormLiq, - 766 to stringsDe.kbvCodeDosageFormLoe, - 767 to stringsDe.kbvCodeDosageFormLot, - 768 to stringsDe.kbvCodeDosageFormLov, - 769 to stringsDe.kbvCodeDosageFormLse, - 770 to stringsDe.kbvCodeDosageFormLta, - 771 to stringsDe.kbvCodeDosageFormLup, - 772 to stringsDe.kbvCodeDosageFormLut, - 773 to stringsDe.kbvCodeDosageFormMil, - 774 to stringsDe.kbvCodeDosageFormMis, - 775 to stringsDe.kbvCodeDosageFormMix, - 776 to stringsDe.kbvCodeDosageFormMrg, - 777 to stringsDe.kbvCodeDosageFormMrp, - 778 to stringsDe.kbvCodeDosageFormMta, - 779 to stringsDe.kbvCodeDosageFormMuw, - 780 to stringsDe.kbvCodeDosageFormNag, - 781 to stringsDe.kbvCodeDosageFormNao, - 782 to stringsDe.kbvCodeDosageFormNas, - 783 to stringsDe.kbvCodeDosageFormNaw, - 784 to stringsDe.kbvCodeDosageFormNds, - 785 to stringsDe.kbvCodeDosageFormNsa, - 786 to stringsDe.kbvCodeDosageFormNtr, - 787 to stringsDe.kbvCodeDosageFormOcu, - 788 to stringsDe.kbvCodeDosageFormOel, - 789 to stringsDe.kbvCodeDosageFormOht, - 790 to stringsDe.kbvCodeDosageFormOvu, - 791 to stringsDe.kbvCodeDosageFormPam, - 792 to stringsDe.kbvCodeDosageFormPas, - 793 to stringsDe.kbvCodeDosageFormPel, - 794 to stringsDe.kbvCodeDosageFormPen, - 795 to stringsDe.kbvCodeDosageFormPer, - 796 to stringsDe.kbvCodeDosageFormPfl, - 797 to stringsDe.kbvCodeDosageFormPft, - 798 to stringsDe.kbvCodeDosageFormPhi, - 799 to stringsDe.kbvCodeDosageFormPhv, - 800 to stringsDe.kbvCodeDosageFormPie, - 801 to stringsDe.kbvCodeDosageFormPif, - 802 to stringsDe.kbvCodeDosageFormPii, - 803 to stringsDe.kbvCodeDosageFormPij, - 804 to stringsDe.kbvCodeDosageFormPik, - 805 to stringsDe.kbvCodeDosageFormPis, - 806 to stringsDe.kbvCodeDosageFormPiv, - 807 to stringsDe.kbvCodeDosageFormPki, - 808 to stringsDe.kbvCodeDosageFormPle, - 809 to stringsDe.kbvCodeDosageFormPlf, - 810 to stringsDe.kbvCodeDosageFormPlg, - 811 to stringsDe.kbvCodeDosageFormPlh, - 812 to stringsDe.kbvCodeDosageFormPli, - 813 to stringsDe.kbvCodeDosageFormPlk, - 814 to stringsDe.kbvCodeDosageFormPls, - 815 to stringsDe.kbvCodeDosageFormPlv, - 816 to stringsDe.kbvCodeDosageFormPpl, - 817 to stringsDe.kbvCodeDosageFormPrs, - 818 to stringsDe.kbvCodeDosageFormPse, - 819 to stringsDe.kbvCodeDosageFormPst, - 820 to stringsDe.kbvCodeDosageFormPud, - 821 to stringsDe.kbvCodeDosageFormPul, - 822 to stringsDe.kbvCodeDosageFormRed, - 823 to stringsDe.kbvCodeDosageFormRek, - 824 to stringsDe.kbvCodeDosageFormRet, - 825 to stringsDe.kbvCodeDosageFormRgr, - 826 to stringsDe.kbvCodeDosageFormRka, - 827 to stringsDe.kbvCodeDosageFormRms, - 828 to stringsDe.kbvCodeDosageFormRsc, - 829 to stringsDe.kbvCodeDosageFormRsu, - 830 to stringsDe.kbvCodeDosageFormRut, - 831 to stringsDe.kbvCodeDosageFormSaf, - 832 to stringsDe.kbvCodeDosageFormSal, - 833 to stringsDe.kbvCodeDosageFormSam, - 834 to stringsDe.kbvCodeDosageFormSch, - 835 to stringsDe.kbvCodeDosageFormSei, - 836 to stringsDe.kbvCodeDosageFormSha, - 837 to stringsDe.kbvCodeDosageFormSir, - 838 to stringsDe.kbvCodeDosageFormSlz, - 839 to stringsDe.kbvCodeDosageFormSmf, - 840 to stringsDe.kbvCodeDosageFormSmt, - 841 to stringsDe.kbvCodeDosageFormSmu, - 842 to stringsDe.kbvCodeDosageFormSpa, - 843 to stringsDe.kbvCodeDosageFormSpf, - 844 to stringsDe.kbvCodeDosageFormSpl, - 845 to stringsDe.kbvCodeDosageFormSpr, - 846 to stringsDe.kbvCodeDosageFormSpt, - 847 to stringsDe.kbvCodeDosageFormSri, - 848 to stringsDe.kbvCodeDosageFormSsu, - 849 to stringsDe.kbvCodeDosageFormSta, - 850 to stringsDe.kbvCodeDosageFormStb, - 851 to stringsDe.kbvCodeDosageFormSti, - 852 to stringsDe.kbvCodeDosageFormStr, - 853 to stringsDe.kbvCodeDosageFormSub, - 854 to stringsDe.kbvCodeDosageFormSue, - 855 to stringsDe.kbvCodeDosageFormSul, - 856 to stringsDe.kbvCodeDosageFormSup, - 857 to stringsDe.kbvCodeDosageFormSus, - 858 to stringsDe.kbvCodeDosageFormSut, - 859 to stringsDe.kbvCodeDosageFormSuv, - 860 to stringsDe.kbvCodeDosageFormSwa, - 861 to stringsDe.kbvCodeDosageFormTab, - 862 to stringsDe.kbvCodeDosageFormTae, - 863 to stringsDe.kbvCodeDosageFormTam, - 864 to stringsDe.kbvCodeDosageFormTee, - 865 to stringsDe.kbvCodeDosageFormTei, - 866 to stringsDe.kbvCodeDosageFormTes, - 867 to stringsDe.kbvCodeDosageFormTin, - 868 to stringsDe.kbvCodeDosageFormTka, - 869 to stringsDe.kbvCodeDosageFormTle, - 870 to stringsDe.kbvCodeDosageFormTmr, - 871 to stringsDe.kbvCodeDosageFormTon, - 872 to stringsDe.kbvCodeDosageFormTpn, - 873 to stringsDe.kbvCodeDosageFormTpo, - 874 to stringsDe.kbvCodeDosageFormTra, - 875 to stringsDe.kbvCodeDosageFormTri, - 876 to stringsDe.kbvCodeDosageFormTro, - 877 to stringsDe.kbvCodeDosageFormTrs, - 878 to stringsDe.kbvCodeDosageFormTrt, - 879 to stringsDe.kbvCodeDosageFormTsa, - 880 to stringsDe.kbvCodeDosageFormTsd, - 881 to stringsDe.kbvCodeDosageFormTse, - 882 to stringsDe.kbvCodeDosageFormTss, - 883 to stringsDe.kbvCodeDosageFormTst, - 884 to stringsDe.kbvCodeDosageFormTsy, - 885 to stringsDe.kbvCodeDosageFormTtr, - 886 to stringsDe.kbvCodeDosageFormTub, - 887 to stringsDe.kbvCodeDosageFormTue, - 888 to stringsDe.kbvCodeDosageFormTup, - 889 to stringsDe.kbvCodeDosageFormTvw, - 890 to stringsDe.kbvCodeDosageFormUta, - 891 to stringsDe.kbvCodeDosageFormVal, - 892 to stringsDe.kbvCodeDosageFormVar, - 893 to stringsDe.kbvCodeDosageFormVcr, - 894 to stringsDe.kbvCodeDosageFormVer, - 895 to stringsDe.kbvCodeDosageFormVge, - 896 to stringsDe.kbvCodeDosageFormVka, - 897 to stringsDe.kbvCodeDosageFormVli, - 898 to stringsDe.kbvCodeDosageFormVov, - 899 to stringsDe.kbvCodeDosageFormVst, - 900 to stringsDe.kbvCodeDosageFormVsu, - 901 to stringsDe.kbvCodeDosageFormVta, - 902 to stringsDe.kbvCodeDosageFormWat, - 903 to stringsDe.kbvCodeDosageFormWga, - 904 to stringsDe.kbvCodeDosageFormWka, - 905 to stringsDe.kbvCodeDosageFormWkm, - 906 to stringsDe.kbvCodeDosageFormWue, - 907 to stringsDe.kbvCodeDosageFormXdg, - 908 to stringsDe.kbvCodeDosageFormXds, - 909 to stringsDe.kbvCodeDosageFormXfe, - 910 to stringsDe.kbvCodeDosageFormXgm, - 911 to stringsDe.kbvCodeDosageFormXha, - 912 to stringsDe.kbvCodeDosageFormXhs, - 913 to stringsDe.kbvCodeDosageFormXnc, - 914 to stringsDe.kbvCodeDosageFormXpk, - 915 to stringsDe.kbvCodeDosageFormXtc, - 916 to stringsDe.kbvCodeDosageFormZam, - 917 to stringsDe.kbvCodeDosageFormZbu, - 918 to stringsDe.kbvCodeDosageFormZcr, - 919 to stringsDe.kbvCodeDosageFormZge, - 920 to stringsDe.kbvCodeDosageFormZka, - 921 to stringsDe.kbvCodeDosageFormZpa - ) -) - -public fun getStrings(locale: Locale): Strings { - val strings = mapOf("de" to stringsDe, "en" to stringsEn, "tr" to stringsTr) - return strings.getOrDefault(locale.language, stringsDe) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/Translatable.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/Translatable.kt deleted file mode 100644 index ad058062..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/strings/Translatable.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common.strings - -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.buildAnnotatedString - -sealed class Translatable { - abstract operator fun invoke(count: Int = 1): String - abstract operator fun invoke(count: Int = 1, vararg args: Any?): String - abstract operator fun invoke(count: Int = 1, vararg args: AnnotatedString): AnnotatedString -} - -data class Singular(val value: String) : Translatable() { - override fun invoke(count: Int): String { - return value - } - - override fun invoke(count: Int, vararg args: Any?): String { - return value.format(*args) - } - - override fun invoke(count: Int, vararg args: AnnotatedString): AnnotatedString = - buildAnnotatedString { - value.split("%s").forEachIndexed { index, s -> - append(s) - if (index < args.size) { - append(args[index]) - } - } - } -} - -data class Plurals(val values: Map) : Translatable() { - enum class Type { - Zero, One, Two, Few, Many, Other - } - - private fun countMapping(count: Int): Type = - when (count) { - 0 -> if (values.containsKey(Type.Zero)) Type.Zero else countMapping(count + 1) - 1 -> if (values.containsKey(Type.One)) Type.One else countMapping(count + 1) - 2 -> if (values.containsKey(Type.Two)) Type.Two else countMapping(count + 1) - in 3..4 -> if (values.containsKey(Type.Few)) Type.Few else countMapping(count + 3) - in 11..99 -> if (values.containsKey(Type.Many)) Type.Many else Type.Other - else -> Type.Other - } - - override fun invoke(count: Int): String { - return requireNotNull(values[countMapping(count)]) { "Couldn't find any plural matching count $count" } - } - - override fun invoke(count: Int, vararg args: Any?): String { - return invoke(count).format(*args) - } - - override fun invoke(count: Int, vararg args: AnnotatedString): AnnotatedString = - buildAnnotatedString { - invoke(count).split("%s").forEachIndexed { index, s -> - append(s) - if (index < args.size) { - append(args[index]) - } - } - } -} - -val LocalStrings = staticCompositionLocalOf { - error("No Strings provided") -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Color.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Color.kt deleted file mode 100644 index 1f90ca8e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Color.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common.theme - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class AppColors( - val green100: Color, - val green200: Color, - val green300: Color, - val green400: Color, - val green500: Color, - val green600: Color, - val green700: Color, - val green800: Color, - val green900: Color, - - val neutral000: Color, - val neutral025: Color, - val neutral050: Color, - val neutral100: Color, - val neutral200: Color, - val neutral300: Color, - val neutral400: Color, - val neutral500: Color, - val neutral600: Color, - val neutral700: Color, - val neutral800: Color, - val neutral900: Color, - val neutral999: Color, - - val primary100: Color, - val primary200: Color, - val primary300: Color, - val primary400: Color, - val primary500: Color, - val primary600: Color, - val primary700: Color, - val primary800: Color, - val primary900: Color, - - val red100: Color, - val red200: Color, - val red300: Color, - val red400: Color, - val red500: Color, - val red600: Color, - val red700: Color, - val red800: Color, - val red900: Color, - - val yellow100: Color, - val yellow200: Color, - val yellow300: Color, - val yellow400: Color, - val yellow500: Color, - val yellow600: Color, - val yellow700: Color, - val yellow800: Color, - val yellow900: Color, - - val scanOverlayErrorOutline: Color, - val scanOverlayErrorFill: Color, - - val scanOverlaySavedOutline: Color, - val scanOverlaySavedFill: Color, - - val scanOverlayHoldOutline: Color, - val scanOverlayHoldFill: Color -) - -val AppColorsThemeLight = AppColors( - green100 = Color(0xFFEBFFF0), - green200 = Color(0xFFC6F6D5), - green300 = Color(0xFF9AE6B4), - green400 = Color(0xFF68D391), - green500 = Color(0xFF48BB78), - green600 = Color(0xFF38A169), - green700 = Color(0xFF2F855A), - green800 = Color(0xFF276749), - green900 = Color(0xFF22543D), - - neutral000 = Color(0xFFFFFFFF), - neutral025 = Color(0xFFFDFDFD), - neutral050 = Color(0xFFFAFAFA), - neutral100 = Color(0xFFF5F5F5), - neutral200 = Color(0xFFEEEEEE), - neutral300 = Color(0xFFE0E0E0), - neutral400 = Color(0xFFBDBDBD), - neutral500 = Color(0xFF9E9E9E), - neutral600 = Color(0xFF757575), - neutral700 = Color(0xFF616161), - neutral800 = Color(0xFF424242), - neutral900 = Color(0xFF212121), - neutral999 = Color(0xFF000000), - - primary100 = Color(0xFFEBF8FF), - primary200 = Color(0xFFBEE3F8), - primary300 = Color(0xFF90CDF4), - primary400 = Color(0xFF63B3ED), - primary500 = Color(0xFF4299E1), - primary600 = Color(0xFF3182CE), - primary700 = Color(0xFF2B6CB0), - primary800 = Color(0xFF2C5282), - primary900 = Color(0xFF2A4365), - - red100 = Color(0xFFFFEBEB), - red200 = Color(0xFFFED7D7), - red300 = Color(0xFFFEB2B2), - red400 = Color(0xFFFC8181), - red500 = Color(0xFFF56565), - red600 = Color(0xFFE53E3E), - red700 = Color(0xFFC53030), - red800 = Color(0xFF9B2C2C), - red900 = Color(0xFF742A2A), - - yellow100 = Color(0xFFFFFFEB), - yellow200 = Color(0xFFFEFCBF), - yellow300 = Color(0xFFFAF089), - yellow400 = Color(0xFFF6E05E), - yellow500 = Color(0xFFECC94B), - yellow600 = Color(0xFFD69E2E), - yellow700 = Color(0xFFB7791F), - yellow800 = Color(0xFF975A16), - yellow900 = Color(0xFF744210), - - scanOverlayErrorOutline = Color(0xFFF86A6A), - scanOverlayErrorFill = Color(0x33E12D39), - scanOverlaySavedOutline = Color(0xFF00FF64), - scanOverlaySavedFill = Color(0x3300D353), - scanOverlayHoldOutline = Color(0xFFFFFFFF), - scanOverlayHoldFill = Color(0x26FFFFFF) -) - -val AppColorsThemeDark = AppColors( - green100 = Color(0x7F22543D), - green200 = Color(0xFF22543D), - green300 = Color(0xFF276749), - green400 = Color(0xFF2F855A), - green500 = Color(0xFF38A169), - green600 = Color(0xFF48BB78), - green700 = Color(0xFF68D391), - green800 = Color(0xFF9AE6B4), - green900 = Color(0xFFC6F6D5), - - neutral000 = Color(0xFF000000), - neutral025 = Color(0xFF0C0C0C), - neutral050 = Color(0xFF121212), - neutral100 = Color(0xFF424242), - neutral200 = Color(0xFF616161), - neutral300 = Color(0xFF757575), - neutral400 = Color(0xFF9E9E9E), - neutral500 = Color(0xFFBDBDBD), - neutral600 = Color(0xFFE0E0E0), - neutral700 = Color(0xFFEEEEEE), - neutral800 = Color(0xFFF5F5F5), - neutral900 = Color(0xFFFAFAFA), - neutral999 = Color(0xFFFFFFFF), - - primary100 = Color(0x7F33517A), - primary200 = Color(0xFF2A4365), - primary300 = Color(0xFF2C5282), - primary400 = Color(0xFF2B6CB0), - primary500 = Color(0xFF3182CE), - primary600 = Color(0xFF4299E1), - primary700 = Color(0xFF63B3ED), - primary800 = Color(0xFF90CDF4), - primary900 = Color(0xFFBEE3F8), - - red100 = Color(0x7F742A2A), - red200 = Color(0xFF742A2A), - red300 = Color(0xFF9B2C2C), - red400 = Color(0xFFC53030), - red500 = Color(0xFFE53E3E), - red600 = Color(0xFFF56565), - red700 = Color(0xFFFC8181), - red800 = Color(0xFFFEB2B2), - red900 = Color(0xFFFED7D7), - - yellow100 = Color(0x4CECC94B), - yellow200 = Color(0xFF744210), - yellow300 = Color(0xFF975A16), - yellow400 = Color(0xFFB7791F), - yellow500 = Color(0xFFD69E2E), - yellow600 = Color(0xFFECC94B), - yellow700 = Color(0xFFF6E05E), - yellow800 = Color(0xFFFAF089), - yellow900 = Color(0xFFFEFCBF), - - scanOverlayErrorOutline = Color(0xFFF86A6A), - scanOverlayErrorFill = Color(0x33E12D39), - scanOverlaySavedOutline = Color(0xFF00FF64), - scanOverlaySavedFill = Color(0x3300D353), - scanOverlayHoldOutline = Color(0xFFFFFFFF), - scanOverlayHoldFill = Color(0x26FFFFFF) -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/PaddingDefaults.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/PaddingDefaults.kt deleted file mode 100644 index 520edb9f..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/PaddingDefaults.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common.theme - -import androidx.compose.ui.unit.dp - -object PaddingDefaults { - val Tiny = 4.dp - val Small = 8.dp - val Medium = 16.dp - val Large = 24.dp -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Theme.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Theme.kt deleted file mode 100644 index 31f3d771..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Theme.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common.theme - -import androidx.compose.desktop.DesktopMaterialTheme -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.platform.Font -import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.common.common.theme.AppTypography -import de.gematik.ti.erp.app.common.common.theme.AppTypographyColors - -@Composable -fun DesktopAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) { - AppColorsThemeDark - } else { - AppColorsThemeLight - } - - val fontFamily = FontFamily( - Font("fonts/NotoSans-Bold.ttf", weight = FontWeight.Bold), - Font("fonts/NotoSans-Medium.ttf", weight = FontWeight.Medium), - Font("fonts/NotoSans-Regular.ttf", weight = FontWeight.Normal), - Font("fonts/NotoSans-SemiBold.ttf", weight = FontWeight.SemiBold) - ) - - DesktopMaterialTheme( - typography = MaterialTheme.typography.copy( - h1 = MaterialTheme.typography.h1.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - h2 = MaterialTheme.typography.h2.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - h3 = MaterialTheme.typography.h3.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - h4 = MaterialTheme.typography.h4.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - h5 = MaterialTheme.typography.h5.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - h6 = MaterialTheme.typography.h6.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - subtitle1 = MaterialTheme.typography.subtitle1.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W500 - ), - subtitle2 = MaterialTheme.typography.subtitle2.copy( - fontFamily = fontFamily, - lineHeight = 1.5.em, - fontWeight = FontWeight.W500 - ), - body1 = MaterialTheme.typography.body1.copy(fontFamily = fontFamily, lineHeight = 1.5.em), - body2 = MaterialTheme.typography.body2.copy(fontFamily = fontFamily, lineHeight = 1.5.em) - ), - colors = Colors( - primary = colors.primary600, - primaryVariant = colors.primary600, - secondary = colors.primary600, - secondaryVariant = colors.primary600, - background = colors.neutral050, - surface = colors.neutral000, - error = colors.red500, - onPrimary = colors.neutral000, - onSecondary = colors.neutral000, - onBackground = colors.neutral999, - onSurface = colors.neutral999, - onError = colors.red900, - isLight = !darkTheme - ), - content = { - val typo = - AppTypography( - body1l = MaterialTheme.typography.body1.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ), - body2l = MaterialTheme.typography.body2.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ), - subtitle1l = MaterialTheme.typography.subtitle1.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ), - subtitle2l = MaterialTheme.typography.subtitle2.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ), - captionl = MaterialTheme.typography.caption.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ), - overlinel = MaterialTheme.typography.overline.copy( - fontFamily = fontFamily, - color = colors.neutral600, - lineHeight = 1.5.em - ) - ) - - CompositionLocalProvider( - LocalAppColors provides colors, - LocalAppTypography provides typo, - content = content - ) - } - ) -} - -object AppTheme { - val colors: AppColors - @Composable - get() = LocalAppColors.current - - val typography: AppTypography - @Composable - get() = LocalAppTypography.current - - val DebugColor = Color(0xFFD71F5F) -} - -private val LocalAppColors = staticCompositionLocalOf { - error("No AppColors provided") -} - -private val LocalAppTypographyColors = staticCompositionLocalOf { - error("No AppTypographyColors provided") -} - -private val LocalAppTypography = staticCompositionLocalOf { - error("No AppTypography provided") -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Type.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Type.kt deleted file mode 100644 index 8efb0da5..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/common/theme/Type.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.common.common.theme - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle - -@Immutable -data class AppTypographyColors( - val body1l: Color, - val body2l: Color, - val subtitle1l: Color, - val subtitle2l: Color, - val captionl: Color -) - -@Immutable -data class AppTypography( - val body1l: TextStyle, - val body2l: TextStyle, - val subtitle1l: TextStyle, - val subtitle2l: TextStyle, - val captionl: TextStyle, - val overlinel: TextStyle -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt deleted file mode 100644 index da889355..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.di - -import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository -import de.gematik.ti.erp.app.communication.repository.LocalDataSource -import de.gematik.ti.erp.app.communication.repository.RemoteDataSource -import de.gematik.ti.erp.app.communication.usecase.CommunicationUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindings.Scope -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton - -fun communicationModule(scope: Scope) = DI.Module("Communication Module") { - bind { scoped(scope).singleton { RemoteDataSource(instance()) } } - bind { scoped(scope).singleton { LocalDataSource() } } - bind { scoped(scope).singleton { DesktopCommunicationRepository(instance(), instance(), instance()) } } - bind { scoped(scope).singleton { CommunicationUseCase(instance(), instance()) } } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt deleted file mode 100644 index e60972f5..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.repository - -import de.gematik.ti.erp.app.fhir.FhirMapper - -class DesktopCommunicationRepository( - private val localDataSource: LocalDataSource, - private val remoteDataSource: RemoteDataSource, - private val mapper: FhirMapper -) { - fun communications() = localDataSource.loadCommunications() - - suspend fun download(): Result = - downloadCommunications() - - suspend fun downloadCommunications(): Result = - remoteDataSource.getAllCommunications().mapCatching { - val communications = mapper.mapFhirBundleToSimpleCommunications(it) - localDataSource.saveCommunications(communications) - } - - suspend fun invalidate() = localDataSource.invalidate() -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/LocalDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/LocalDataSource.kt deleted file mode 100644 index 13f8be77..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/LocalDataSource.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.repository - -import de.gematik.ti.erp.app.prescription.repository.SimpleCommunication -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -class LocalDataSource { - private val communications = MutableStateFlow(emptyList()) - private val lock = Mutex() - - suspend fun saveCommunications(communications: List) = lock.withLock { - val ids = communications.map { it.id } - this.communications.value = this.communications.value.filter { it.id !in ids } + communications - } - - fun loadCommunications(): Flow> { - return communications - } - - suspend fun invalidate() = lock.withLock { - communications.value = emptyList() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/RemoteDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/RemoteDataSource.kt deleted file mode 100644 index cf50fa17..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/RemoteDataSource.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.repository - -import de.gematik.ti.erp.app.api.ErpService -import de.gematik.ti.erp.app.core.safeApiCall -import org.hl7.fhir.r4.model.Bundle - -class RemoteDataSource( - private val service: ErpService -) { - suspend fun getAllCommunications(): Result = - safeApiCall("Error getting communications") { service.getAllCommunications() } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/SimpleCommunication.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/SimpleCommunication.kt deleted file mode 100644 index be814a26..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/SimpleCommunication.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository - -import java.time.LocalDateTime -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -sealed class SimpleCommunication { - abstract val id: String - abstract val profile: CommunicationProfile - abstract val sent: LocalDateTime? -} - -data class SimpleCommunicationWithPharmacy( - override val id: String, - override val profile: CommunicationProfile, - override val sent: LocalDateTime?, - val basedOnTaskWithId: String, - val telematicsId: String, // pharmacy id - val userId: String, // refer to KVNR (e.g. X123456789) - val payload: CommunicationPayloadInbox? -) : SimpleCommunication() - -enum class CommunicationProfile { - DispenseRequest, Reply -} - -@Serializable -data class CommunicationPayloadInbox( - @SerialName("version") val version: String = "1", - @SerialName("supplyOptionsType") val supplyOptionsType: CommunicationSupplyOption, - @SerialName("info_text") val infoText: String, - @SerialName("url") val url: String? = null, - @SerialName("pickUpCodeHR") val pickUpCodeHR: String? = null, - @SerialName("pickUpCodeDMC") val pickUpCodeDMC: String? = null -) - -@Serializable -enum class CommunicationSupplyOption { - @SerialName("onPremise") - OnPremise, - - @SerialName("shipment") - Shipment, - - @SerialName("delivery") - Delivery -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationScreen.kt deleted file mode 100644 index 46c46542..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationScreen.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.ui - -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowForward -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption.Delivery -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption.OnPremise -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption.Shipment -import de.gematik.ti.erp.app.rememberScope -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import kotlinx.coroutines.flow.collect -import org.kodein.di.bind -import org.kodein.di.compose.rememberInstance -import org.kodein.di.compose.subDI -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton - -@Composable -fun CommunicationScreen() { - val scope = rememberScope() - - subDI(diBuilder = { - bind { scoped(scope).singleton { CommunicationViewModel(instance(), instance()) } } - }) { - val communicationViewModel by rememberInstance() - val state by produceState(communicationViewModel.defaultState) { - communicationViewModel.screenState().collect { - value = it - } - } - - val lazyListState = rememberLazyListState() - val scrollbarAdapter = rememberScrollbarAdapter(lazyListState) - - val dtFormatter = remember { DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT) } - - if (state.pharmacyCommunications.isNotEmpty()) { - SelectionContainer { - Box(Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.widthIn(max = 560.dp).align(Alignment.Center), - state = lazyListState - ) { - itemsIndexed(state.pharmacyCommunications) { index, it -> - CommunicationEntry( - modifier = Modifier.fillMaxWidth().padding(PaddingDefaults.Medium), - medication = it.name, - type = it.supplyOption, - code = it.pickUpCode, - url = it.url, - infoText = it.infoText, - sender = it.sender, - recipient = it.recipient, - sent = it.sent?.format(dtFormatter) - ) - } - } - VerticalScrollbar( - scrollbarAdapter, - modifier = Modifier.align(Alignment.CenterEnd).padding(horizontal = 1.dp).fillMaxHeight() - ) - } - } - } - } -} - -@Composable -private fun CommunicationEntry( - modifier: Modifier, - medication: String, - type: SupplyOption?, - code: String?, - url: String?, - infoText: String?, - sender: String, - recipient: String, - sent: String? -) { - Column(modifier, verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Tiny)) { - Text(medication, style = MaterialTheme.typography.h6) - Row(horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Tiny)) { - type?.let { - Chip( - when (type) { - OnPremise -> App.strings.desktopCommunicationOnPremise() - Shipment -> App.strings.desktopCommunicationShipment() - Delivery -> App.strings.desktopCommunicationDelivery() - } - ) - } - code?.let { Chip(it) } - url?.let { Chip(it) } - } - infoText?.let { - Text(it, style = MaterialTheme.typography.body1) - } - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Tiny), - verticalAlignment = Alignment.CenterVertically - ) { - Text(sender, style = AppTheme.typography.body2l) - Icon(Icons.Rounded.ArrowForward, null, tint = AppTheme.colors.neutral400, modifier = Modifier.size(12.dp)) - Text(recipient, style = AppTheme.typography.body2l) - } - sent?.let { Text(it, style = AppTheme.typography.body2l) } - } -} - -@Composable -private fun Chip( - text: String -) { - Box( - Modifier - .background(AppTheme.colors.neutral100, shape = CircleShape) - .padding(horizontal = PaddingDefaults.Small, vertical = PaddingDefaults.Tiny / 2) - ) { - Text(text, style = AppTheme.typography.captionl) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt deleted file mode 100644 index e7222863..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.ui - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.communication.ui.model.CommunicationScreenData -import de.gematik.ti.erp.app.communication.usecase.CommunicationUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map - -class CommunicationViewModel( - private val dispatchProvider: DispatchProvider, - private val communicationUseCase: CommunicationUseCase -) { - val defaultState = - CommunicationScreenData.State(emptyList()) - - @OptIn(ExperimentalCoroutinesApi::class) - fun screenState(): Flow = - communicationUseCase.pharmacyCommunications().map { - CommunicationScreenData.State(it) - }.flowOn(dispatchProvider.unconfined) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/model/CommunicationScreenData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/model/CommunicationScreenData.kt deleted file mode 100644 index 01b94620..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/model/CommunicationScreenData.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.ui.model - -import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData - -object CommunicationScreenData { - @Immutable - data class State( - val pharmacyCommunications: List - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt deleted file mode 100644 index 497d515c..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.usecase - -import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData -import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption -import de.gematik.ti.erp.app.prescription.repository.CommunicationProfile.Reply -import de.gematik.ti.erp.app.prescription.repository.CommunicationSupplyOption -import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository -import de.gematik.ti.erp.app.prescription.repository.SimpleCommunicationWithPharmacy -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -class CommunicationUseCase( - private val communicationRepository: DesktopCommunicationRepository, - private val prescriptionRepository: DesktopPrescriptionRepository -) { - fun pharmacyCommunications(): Flow> = - communicationRepository.communications().map { - it.mapNotNull { it as? SimpleCommunicationWithPharmacy } - }.combine(prescriptionRepository.tasks()) { communications, tasks -> - val medications = tasks.associate { it.taskId to it.medicationText } - - communications.map { - mapSimpleCommunication(it, medications[it.basedOnTaskWithId]) - } - } - - // - // mapper - // - - private fun mapSimpleCommunication(com: SimpleCommunicationWithPharmacy, medicationName: String?) = - CommunicationUseCaseData.Communication( - name = medicationName ?: com.basedOnTaskWithId, - sender = if (com.profile == Reply) com.telematicsId else com.userId, - recipient = if (com.profile == Reply) com.userId else com.telematicsId, - infoText = com.payload?.infoText, - supplyOption = when (com.payload?.supplyOptionsType) { - CommunicationSupplyOption.OnPremise -> SupplyOption.OnPremise - CommunicationSupplyOption.Shipment -> SupplyOption.Shipment - CommunicationSupplyOption.Delivery -> SupplyOption.Delivery - else -> null - }, - pickUpCode = (com.payload?.pickUpCodeHR ?: com.payload?.pickUpCodeDMC)?.takeIf { it.isNotBlank() }, - url = com.payload?.url?.takeIf { it.isNotBlank() }, - sent = com.sent - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/model/CommunicationUseCaseData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/model/CommunicationUseCaseData.kt deleted file mode 100644 index 105dc6e6..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/model/CommunicationUseCaseData.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.communication.usecase.model - -import androidx.compose.runtime.Stable -import java.time.LocalDateTime - -object CommunicationUseCaseData { - @Stable - data class Communication( - val name: String, - val sender: String, - val recipient: String, - val infoText: String?, - val supplyOption: SupplyOption?, - val pickUpCode: String?, - val url: String?, - val sent: LocalDateTime? - ) { - enum class SupplyOption { - OnPremise, Shipment, Delivery - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/FhirConverterFactory.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/FhirConverterFactory.kt deleted file mode 100644 index 95467864..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/FhirConverterFactory.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.core - -import ca.uhn.fhir.parser.IParser -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.ResponseBody -import org.hl7.fhir.r4.model.Resource -import retrofit2.Converter -import retrofit2.Retrofit -import java.lang.reflect.Type - -class FhirConverterFactory(private val parser: IParser) : Converter.Factory() { - - companion object { - fun create(parser: IParser) = FhirConverterFactory(parser) - } - - override fun responseBodyConverter( - type: Type, - annotations: Array, - retrofit: Retrofit - ): Converter { - return FhirBundleConverter(parser) - } - - override fun requestBodyConverter( - type: Type, - parameterAnnotations: Array, - methodAnnotations: Array, - retrofit: Retrofit - ): Converter { - return FhirResourceConverter(parser) - } - - class FhirBundleConverter(private val parser: IParser) : Converter { - - override fun convert(value: ResponseBody): Any? { - return parser.parseResource(value.byteStream()) - } - } - - class FhirResourceConverter(private val parser: IParser) : Converter { - - override fun convert(value: Resource): RequestBody { - val result = parser.setPrettyPrint(false).encodeResourceToString(value) - // TODO Remove this replace as soon as the spec is updated and the backend handles the accessCode itself - return result - .replace("\$accept", "/\$accept") - .toRequestBody("application/fhir+json".toMediaType()) - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/PrefixedLogger.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/PrefixedLogger.kt deleted file mode 100644 index 81ae57e1..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/PrefixedLogger.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.core - -import io.github.aakira.napier.Napier -import okhttp3.logging.HttpLoggingInterceptor - -class NapierLogger() : HttpLoggingInterceptor.Logger { - override fun log(message: String) { - Napier.d(message) - } -} - -class PrefixedLogger(val prefix: String) : HttpLoggingInterceptor.Logger { - override fun log(message: String) { - Napier.d("[$prefix] $message") - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/SafeApiCall.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/SafeApiCall.kt deleted file mode 100644 index f8cd64ba..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/core/SafeApiCall.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.core - -import retrofit2.Response -import java.io.IOException - -class ApiCallException(message: String, val response: Response<*>) : IOException(message) - -/** - * Wraps a remote call in a try catch and returns [Result.Error] with an [IOException] in case [call] couldn't be executed. - * In case of a successful response, the [Result.Success] contains the body of it. - */ -suspend fun safeApiCall( - errorMessage: String, - call: suspend () -> Response -): Result = - try { - val response = call() - if (response.isSuccessful) { - requireNotNull(response.body()).let { Result.success(it) } - } else { - Result.failure( - ApiCallException("Error executing safe api call ${response.code()} ${response.message()}", response) - ) - } - } catch (e: Exception) { - // An exception was thrown when calling the API so we're converting this to an [IOException] - Result.failure(IOException(errorMessage, e)) - } - -suspend fun safeApiCallNullable( - errorMessage: String, - call: suspend () -> Response -): Result = - try { - val response = call() - if (response.isSuccessful) { - response.body()?.let { Result.success(it) } ?: Result.success(null) - } else { - Result.failure( - ApiCallException("Error executing safe api call ${response.code()} ${response.message()}", response) - ) - } - } catch (e: Exception) { - // An exception was thrown when calling the API so we're converting this to an [IOException] - Result.failure(IOException(errorMessage, e)) - } - -/** - * This safeApi call should only be used if it's necessary to do all error handling on its own apart from an io exception. - */ -suspend fun safeApiCallRaw( - errorMessage: String, - call: suspend () -> Result -): Result = - try { - call() - } catch (e: Exception) { - // An exception was thrown when calling the API so we're converting this to an IOException - Result.failure(IOException(errorMessage, e)) - } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/FhirMapper.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/FhirMapper.kt deleted file mode 100644 index 6c2c0386..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/FhirMapper.kt +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") - -package de.gematik.ti.erp.app.fhir - -import ca.uhn.fhir.parser.IParser -import de.gematik.ti.erp.app.prescription.repository.CommunicationPayloadInbox -import de.gematik.ti.erp.app.prescription.repository.CommunicationProfile -import de.gematik.ti.erp.app.prescription.repository.SimpleCommunication -import de.gematik.ti.erp.app.prescription.repository.SimpleCommunicationWithPharmacy -import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent -import de.gematik.ti.erp.app.prescription.repository.model.SimpleMedicationDispense -import de.gematik.ti.erp.app.prescription.repository.model.SimpleTask -import de.gematik.ti.erp.app.utils.convertFhirDateToLocalDate -import de.gematik.ti.erp.app.utils.convertFhirDateToLocalDateTime -import java.time.LocalDate -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import org.hl7.fhir.r4.model.Address -import org.hl7.fhir.r4.model.AuditEvent -import org.hl7.fhir.r4.model.BooleanType -import org.hl7.fhir.r4.model.Bundle -import org.hl7.fhir.r4.model.CodeType -import org.hl7.fhir.r4.model.Coding -import org.hl7.fhir.r4.model.ContactPoint -import org.hl7.fhir.r4.model.DateType -import org.hl7.fhir.r4.model.DomainResource -import org.hl7.fhir.r4.model.HumanName -import org.hl7.fhir.r4.model.MedicationDispense -import org.hl7.fhir.r4.model.MedicationDispense.MedicationDispensePerformerComponent -import org.hl7.fhir.r4.model.MedicationRequest -import org.hl7.fhir.r4.model.Reference -import org.hl7.fhir.r4.model.StringType -import org.hl7.fhir.r4.model.Task - -typealias FhirPractitioner = org.hl7.fhir.r4.model.Practitioner -typealias FhirMedication = org.hl7.fhir.r4.model.Medication -typealias FhirMedicationRequest = MedicationRequest -typealias FhirPatient = org.hl7.fhir.r4.model.Patient -typealias FhirOrganization = org.hl7.fhir.r4.model.Organization -typealias FhirCoverage = org.hl7.fhir.r4.model.Coverage -typealias FhirCommunication = org.hl7.fhir.r4.model.Communication -typealias FhirTask = org.hl7.fhir.r4.model.Task - -inline fun Bundle.extractResources(): List? { - return entry?.let { it -> - it.filter { it.resource is T } - .map { it.resource as T } - } -} - -inline fun Bundle.extractSingleResource(): T? { - return entry?.firstOrNull()?.let { it as T } -} - -inline fun Bundle.BundleEntryComponent.extractResource(): T? { - return entries()?.let { it -> - it.filter { it.resource is T } - .map { it.resource as T } - .firstOrNull() - } -} - -inline fun Bundle.BundleEntryComponent.extractResourceForReference( - reference: String -): T? { - return entries().let { it -> - it.filter { - it.resource is T && it.resource.id == reference - }.map { - it.resource as T - } - .firstOrNull() - } -} - -fun Bundle.BundleEntryComponent.entries(): List { - return resource.getChildByName("entry").values as List -} - -fun FhirTask.extractKBVBundleReference(): String? { - return ( - input.find { - val code = (it as Task.ParameterComponent).type.coding[0].code - val system = it.type.coding[0].system - - code == "2" && system == "https://gematik.de/fhir/CodeSystem/Documenttype" - }?.value as Reference - ).reference -} - -fun MedicationRequest.findReferences() = mapOf( - "medication" to medicationReference.reference, - "patient" to subject.reference, - "practitioner" to requester.reference, - "insuranceReference" to insurance[0].reference -) - -// extracts the very first dosage instruction -fun MedicationRequest.extractDosageInstructions(): String? { - val dosageInstruction = dosageInstruction.firstOrNull() - - val dosageFlag = - (dosageInstruction?.getExtensionByUrl("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_DosageFlag")?.value as? BooleanType?)?.value - return dosageInstruction?.let { - if (dosageFlag != null) { - // dosage flag is set - if (dosageFlag) { - dosageInstruction.text ?: "" - } else { - null - } - } else { - dosageInstruction.text - } - } -} - -fun Bundle.extractKBVBundle(reference: String): Bundle.BundleEntryComponent? { - val cleanRefId = - if (reference.first() == '#') { - reference.subSequence(1, reference.length) - } else { - reference - } - - // BUG: Workaround for https://github.com/hapifhir/org.hl7.fhir.core/pull/12 - return entry.find { it.resource.id.removePrefix("urn:uuid:") == cleanRefId } -} - -fun FhirTask.prescriptionId(): String? { - identifier.forEach { - if (it.hasSystem()) { - if (it.system == "https://gematik.de/fhir/NamingSystem/PrescriptionID") { - return it.value - } - } - } - return null -} - -class FhirMapper( - private val fhirParser: IParser, - private val json: Json -) { - fun parseBundle(bundle: String): Bundle = fhirParser.parseResource(bundle) as Bundle - - private fun extractDateExtension(task: FhirTask, extensionUrl: String): LocalDate? { - val fhirDate = - task.getExtensionByUrl(extensionUrl) - ?.value as DateType? - - return LocalDate.parse(fhirDate?.valueAsString) - } - - fun parseTaskIds(bundle: Bundle): List { - return bundle.extractResources()?.map { - it.idElement.idPart - } ?: listOf() - } - - /** - * Maps Task and KBV Bundle together to a complete Task Entity - * - * @param bundle the bundle consists of a Task which is referencing an included KBV Bundle. - */ - fun mapFhirBundleToTaskWithKBVBundle(bundle: Bundle): SimpleTask = - bundle.extractResources()?.firstOrNull()?.let { fhirTask -> - fhirTask.extractKBVBundleReference()?.let { kbvBundleReference -> - val kbvBundle = requireNotNull(bundle.extractKBVBundle(kbvBundleReference)) - - var _fhirMedication: FhirMedication? = null - var _fhirMedicationRequest: FhirMedicationRequest? = null - var _fhirOrganization: FhirOrganization? = null - var _fhirPractitioner: FhirPractitioner? = null - - kbvBundle.entries().map { - when (val resource = it.resource) { - is FhirMedication -> _fhirMedication = resource - is FhirMedicationRequest -> _fhirMedicationRequest = resource - is FhirOrganization -> _fhirOrganization = resource - is FhirPractitioner -> _fhirPractitioner = resource - } - } - - val fhirMedication = requireNotNull(_fhirMedication) - val fhirMedicationRequest = requireNotNull(_fhirMedicationRequest) - val fhirOrganization = requireNotNull(_fhirOrganization) - val fhirPractitioner = requireNotNull(_fhirPractitioner) - - SimpleTask( - taskId = fhirTask.idElement.idPart, - lastModified = fhirTask.lastModified.convertFhirDateToLocalDateTime(), - organization = fhirOrganization.name - ?: fhirPractitioner.nameFirstRep.nameAsSingleString, - medicationText = fhirMedication.code.text, - expiresOn = requireNotNull( - extractDateExtension( - fhirTask, - "https://gematik.de/fhir/StructureDefinition/ExpiryDate" - ) - ), - acceptUntil = requireNotNull( - extractDateExtension( - fhirTask, - "https://gematik.de/fhir/StructureDefinition/AcceptDate" - ) - ), - authoredOn = fhirMedicationRequest.authoredOn.convertFhirDateToLocalDateTime(), - status = fhirTask.status.definition, - rawKBVBundle = kbvBundle.resource as Bundle - ) - } ?: error("KBV Bundle not found!") - } ?: error("No task found!") - - /** - * Throws an exception if the bundle couldn't be parsed. - */ - fun parseKBVBundle(rawKBVBundle: ByteArray): Bundle { - return fhirParser.parseResource(rawKBVBundle.decodeToString()) as Bundle - } - - fun mapFhirBundleToAuditEvents(fhirBundle: Bundle): List { - return fhirBundle.extractResources()?.map { - SimpleAuditEvent( - id = it.idElement.idPart, - locale = if (it.language.isNullOrEmpty()) "de" else it.language, - text = it.text.div.allText(), - timestamp = it.recorded.convertFhirDateToLocalDateTime(), - taskId = it.entity[0].what.referenceElement.idPart - ) - } ?: error("No AuditEvent found in given Bundle $fhirBundle") - } - - fun mapFhirMedicationDispenseToSimpleMedicationDispense(fhirBundle: Bundle): List { - return fhirBundle.extractResources()?.map { - SimpleMedicationDispense( - id = it.idElement.idPart, - taskId = it.identifier.first().value, - patientIdentifier = it.subject.identifier.value, - wasSubstituted = it.substitution.wasSubstituted, - medicationDetail = (it.contained.first() as FhirMedication).mapToUi(), - dosageInstruction = it.dosageInstruction.firstOrNull()?.text, - performer = (it.performer.first() as MedicationDispensePerformerComponent).actor.identifier.value, - whenHandedOver = it.whenHandedOver.convertFhirDateToLocalDateTime() - ) - } ?: error("No MedicationDispense found in given Bundle $fhirBundle") - } - - private val COMMUNICATION_TYPE_DISP_REQ = "https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq" - private val COMMUNICATION_TYPE_REPLY = "https://gematik.de/fhir/StructureDefinition/ErxCommunicationReply" - - fun mapFhirBundleToSimpleCommunications(bundle: Bundle): List { - return bundle.extractResources()?.mapNotNull { fhirCommunication -> - when (fhirCommunication.meta.profile.first().value.split("|").first()) { - COMMUNICATION_TYPE_DISP_REQ -> mapToSimpleCommunicationWithPharmacyAsDispReq(fhirCommunication) - COMMUNICATION_TYPE_REPLY -> mapToSimpleCommunicationWithPharmacyAsReply(fhirCommunication) - else -> null // ignore other communications - } - } ?: error("No communication found!") - } - - private fun extractTaskIdFromReference(reference: String): String { - return reference.split("/")[1] - } - - @OptIn(ExperimentalSerializationApi::class) - private fun mapToSimpleCommunicationWithPharmacyAsDispReq( - fhirCommunication: FhirCommunication - ) = SimpleCommunicationWithPharmacy( - id = fhirCommunication.idElement.idPart, - profile = CommunicationProfile.DispenseRequest, - basedOnTaskWithId = extractTaskIdFromReference(fhirCommunication.basedOn.first().reference), - sent = fhirCommunication.sent?.convertFhirDateToLocalDateTime(), - telematicsId = fhirCommunication.recipient.first().identifier.value, - userId = fhirCommunication.sender.identifier.value, - payload = runCatching { - json.decodeFromString(fhirCommunication.payload.first().content.toString()) - }.getOrNull() - ) - - @OptIn(ExperimentalSerializationApi::class) - private fun mapToSimpleCommunicationWithPharmacyAsReply( - fhirCommunication: FhirCommunication - ) = SimpleCommunicationWithPharmacy( - id = fhirCommunication.idElement.idPart, - profile = CommunicationProfile.Reply, - basedOnTaskWithId = extractTaskIdFromReference(fhirCommunication.basedOn.first().reference), - sent = fhirCommunication.sent?.convertFhirDateToLocalDateTime(), - userId = fhirCommunication.recipient.first().identifier.value, - telematicsId = fhirCommunication.sender.identifier.value, - payload = json.decodeFromString(fhirCommunication.payload.first().content.toString()) - ) -} - -data class PatientDetail( - val name: String? = null, - val address: String? = null, - val birthdate: LocalDate? = null, - val insuranceIdentifier: String? = null // code == GKV -) - -data class PractitionerDetail( - val name: String? = null, - val qualification: String? = null, - val practitionerIdentifier: String? = null // code == LANR (long term practitioner id) -) - -data class MedicationDetail( - val text: String? = null, - val dosageCode: String? = null, - val normSizeCode: String? = null, - val uniqueIdentifier: String? = null // PZN -) - -data class InsuranceCompanyDetail( - val name: String? = null, - val statusCode: String? = null -) - -data class OrganizationDetail( - val name: String? = null, - val address: String? = null, - val uniqueIdentifier: String? = null, // BSNR - val phone: String? = null, - val mail: String? = null -) - -// reference: https://simplifier.net/erezept/kbvprerpprescription -data class MedicationRequestDetail( - val dateOfAccident: LocalDate? = null, // unfalltag - val location: String? = null, // unfallbetrieb - val emergencyFee: Boolean? = null, // emergency service fee = notfallgebuehr - val substitutionAllowed: Boolean = false, - val dosageInstruction: String? = null -) - -fun FhirPatient.mapToUi(): PatientDetail = PatientDetail( - name = this.name.find { it.use == HumanName.NameUse.OFFICIAL }?.nameAsSingleString, - address = this.address.find { it.type == Address.AddressType.BOTH }?.let { - val lines = it.line?.map { l -> l?.value } ?: emptyList() - val address = lines + it.postalCode + it.city - - address.filterNot { a -> a.isNullOrBlank() }.takeIf { a -> a.isNotEmpty() } - ?.joinToString(", ") - }, - birthdate = this.birthDate?.let { LocalDate.from(it.convertFhirDateToLocalDate()) }, - insuranceIdentifier = this.identifier.firstOrNull()?.value -) - -fun FhirPractitioner.mapToUi(): PractitionerDetail = PractitionerDetail( - name = this.name.find { it.use == HumanName.NameUse.OFFICIAL }?.nameAsSingleString, - qualification = this.qualification.find { it.code?.hasText() == true }?.code?.text, - practitionerIdentifier = this.identifier.firstOrNull()?.value -) - -fun FhirMedication.mapToUi(): MedicationDetail = MedicationDetail( - text = this.code?.text, - dosageCode = this.form?.coding?.find { it.system == "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM" }?.code, - normSizeCode = (this.getExtensionByUrl("http://fhir.de/StructureDefinition/normgroesse")?.value as? CodeType?)?.value, - uniqueIdentifier = this.code?.coding?.find { it.system == "http://fhir.de/CodeSystem/ifa/pzn" }?.code -) - -fun FhirCoverage.mapToUi() = InsuranceCompanyDetail( - name = this.payorFirstRep?.display, - statusCode = (this.getExtensionByUrl("http://fhir.de/StructureDefinition/gkv/versichertenart")?.value as? Coding?)?.code -) - -fun FhirOrganization.mapToUi() = OrganizationDetail( - name = this.name, - address = this.address.find { it.type == Address.AddressType.BOTH }?.line?.firstOrNull()?.value, - uniqueIdentifier = this.identifier?.find { it.system == "https://fhir.kbv.de/NamingSystem/KBV_NS_Base_BSNR" }?.value, - phone = this.telecom?.find { it.system == ContactPoint.ContactPointSystem.PHONE }?.value, - mail = this.telecom?.find { it.system == ContactPoint.ContactPointSystem.EMAIL }?.value -) - -fun FhirMedicationRequest.mapToUi() = MedicationRequestDetail( - dateOfAccident = ( - this.getExtensionByUrl("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Accident") - ?.getExtensionByUrl("unfalltag")?.value as? DateType? - ) - ?.value - ?.let { LocalDate.from(it.convertFhirDateToLocalDate()) }, - location = ( - this.getExtensionByUrl("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Accident") - ?.getExtensionByUrl("unfallbetrieb")?.value as? StringType? - )?.value, - emergencyFee = ( - this.getExtensionByUrl("https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_EmergencyServicesFee") - ?.value as? BooleanType? - )?.value, - substitutionAllowed = this.substitution.allowedBooleanType.booleanValue(), - dosageInstruction = this.extractDosageInstructions() -) - -fun Bundle.extractPatient() = this.extractResources()?.firstOrNull()?.mapToUi() - -fun Bundle.extractMedication() = - this.extractResources()?.firstOrNull()?.mapToUi() - -fun Bundle.extractMedicationRequest() = - this.extractResources()?.firstOrNull()?.mapToUi() - -fun Bundle.extractPractitioner() = - this.extractResources()?.firstOrNull()?.mapToUi() - -fun Bundle.extractInsurance() = this.extractResources()?.firstOrNull()?.mapToUi() - -fun Bundle.extractOrganization() = - this.extractResources()?.firstOrNull()?.mapToUi() diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/KBVCodeMapping.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/KBVCodeMapping.kt deleted file mode 100644 index 99be28bf..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/fhir/KBVCodeMapping.kt +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.fhir - -import de.gematik.ti.erp.app.common.strings.Strings - -fun Strings.statusMapping() = mapOf( - "1" to kbvMemberStatus1, - "3" to kbvMemberStatus3, - "5" to kbvMemberStatus5 -) - -fun Strings.normSizeMapping() = mapOf( - "KA" to kbvNormSizeKa, - "KTP" to kbvNormSizeKtp, - "N1" to kbvNormSizeN1, - "N2" to kbvNormSizeN2, - "N3" to kbvNormSizeN3, - "NB" to kbvNormSizeNb, - "Sonstiges" to kbvNormSizeSonstiges -) - -fun Strings.codeToDosageFormMapping() = mapOf( - "AEO" to kbvCodeDosageFormAeo, - "AMP" to kbvCodeDosageFormAmp, - "APA" to kbvCodeDosageFormApa, - "ASN" to kbvCodeDosageFormAsn, - "ASO" to kbvCodeDosageFormAso, - "ATO" to kbvCodeDosageFormAto, - "ATR" to kbvCodeDosageFormAtr, - "AUB" to kbvCodeDosageFormAub, - "AUC" to kbvCodeDosageFormAuc, - "AUG" to kbvCodeDosageFormAug, - "AUS" to kbvCodeDosageFormAus, - "BAD" to kbvCodeDosageFormBad, - "BAL" to kbvCodeDosageFormBal, - "BAN" to kbvCodeDosageFormBan, - "BEU" to kbvCodeDosageFormBeu, - "BIN" to kbvCodeDosageFormBin, - "BON" to kbvCodeDosageFormBon, - "BPL" to kbvCodeDosageFormBpl, - "BRE" to kbvCodeDosageFormBre, - "BTA" to kbvCodeDosageFormBta, - "CRE" to kbvCodeDosageFormCre, - "DFL" to kbvCodeDosageFormDfl, - "DIL" to kbvCodeDosageFormDil, - "DIS" to kbvCodeDosageFormDis, - "DKA" to kbvCodeDosageFormDka, - "DOS" to kbvCodeDosageFormDos, - "DRA" to kbvCodeDosageFormDra, - "DRM" to kbvCodeDosageFormDrm, - "DSC" to kbvCodeDosageFormDsc, - "DSS" to kbvCodeDosageFormDss, - "EDP" to kbvCodeDosageFormEdp, - "EIN" to kbvCodeDosageFormEin, - "ELE" to kbvCodeDosageFormEle, - "ELI" to kbvCodeDosageFormEli, - "EMU" to kbvCodeDosageFormEmu, - "ESS" to kbvCodeDosageFormEss, - "ESU" to kbvCodeDosageFormEsu, - "EXT" to kbvCodeDosageFormExt, - "FBE" to kbvCodeDosageFormFbe, - "FBW" to kbvCodeDosageFormFbw, - "FDA" to kbvCodeDosageFormFda, - "FER" to kbvCodeDosageFormFer, - "FET" to kbvCodeDosageFormFet, - "FLA" to kbvCodeDosageFormFla, - "FLE" to kbvCodeDosageFormFle, - "FLU" to kbvCodeDosageFormFlu, - "FMR" to kbvCodeDosageFormFmr, - "FOL" to kbvCodeDosageFormFol, - "FRB" to kbvCodeDosageFormFrb, - "FSE" to kbvCodeDosageFormFse, - "FTA" to kbvCodeDosageFormFta, - "GEK" to kbvCodeDosageFormGek, - "GEL" to kbvCodeDosageFormGel, - "GLI" to kbvCodeDosageFormGli, - "GLO" to kbvCodeDosageFormGlo, - "GMR" to kbvCodeDosageFormGmr, - "GPA" to kbvCodeDosageFormGpa, - "GRA" to kbvCodeDosageFormGra, - "GSE" to kbvCodeDosageFormGse, - "GUL" to kbvCodeDosageFormGul, - "HAS" to kbvCodeDosageFormHas, - "HKM" to kbvCodeDosageFormHkm, - "HKP" to kbvCodeDosageFormHkp, - "HPI" to kbvCodeDosageFormHpi, - "HVW" to kbvCodeDosageFormHvw, - "IFA" to kbvCodeDosageFormIfa, - "IFB" to kbvCodeDosageFormIfb, - "IFD" to kbvCodeDosageFormIfd, - "IFE" to kbvCodeDosageFormIfe, - "IFF" to kbvCodeDosageFormIff, - "IFK" to kbvCodeDosageFormIfk, - "IFL" to kbvCodeDosageFormIfl, - "IFS" to kbvCodeDosageFormIfs, - "IHA" to kbvCodeDosageFormIha, - "IHP" to kbvCodeDosageFormIhp, - "IIE" to kbvCodeDosageFormIie, - "IIL" to kbvCodeDosageFormIil, - "IIM" to kbvCodeDosageFormIim, - "IKA" to kbvCodeDosageFormIka, - "ILO" to kbvCodeDosageFormIlo, - "IMP" to kbvCodeDosageFormImp, - "INF" to kbvCodeDosageFormInf, - "INH" to kbvCodeDosageFormInh, - "INI" to kbvCodeDosageFormIni, - "INL" to kbvCodeDosageFormInl, - "INS" to kbvCodeDosageFormIns, - "IST" to kbvCodeDosageFormIst, - "ISU" to kbvCodeDosageFormIsu, - "IUP" to kbvCodeDosageFormIup, - "KAN" to kbvCodeDosageFormKan, - "KAP" to kbvCodeDosageFormKap, - "KAT" to kbvCodeDosageFormKat, - "KDA" to kbvCodeDosageFormKda, - "KEG" to kbvCodeDosageFormKeg, - "KER" to kbvCodeDosageFormKer, - "KGU" to kbvCodeDosageFormKgu, - "KID" to kbvCodeDosageFormKid, - "KII" to kbvCodeDosageFormKii, - "KKS" to kbvCodeDosageFormKks, - "KLI" to kbvCodeDosageFormKli, - "KLT" to kbvCodeDosageFormKlt, - "KMP" to kbvCodeDosageFormKmp, - "KMR" to kbvCodeDosageFormKmr, - "KOD" to kbvCodeDosageFormKod, - "KOM" to kbvCodeDosageFormKom, - "KON" to kbvCodeDosageFormKon, - "KPG" to kbvCodeDosageFormKpg, - "KRI" to kbvCodeDosageFormKri, - "KSS" to kbvCodeDosageFormKss, - "KSU" to kbvCodeDosageFormKsu, - "KTA" to kbvCodeDosageFormKta, - "LAN" to kbvCodeDosageFormLan, - "LII" to kbvCodeDosageFormLii, - "LIQ" to kbvCodeDosageFormLiq, - "LOE" to kbvCodeDosageFormLoe, - "LOT" to kbvCodeDosageFormLot, - "LOV" to kbvCodeDosageFormLov, - "LSE" to kbvCodeDosageFormLse, - "LTA" to kbvCodeDosageFormLta, - "LUP" to kbvCodeDosageFormLup, - "LUT" to kbvCodeDosageFormLut, - "MIL" to kbvCodeDosageFormMil, - "MIS" to kbvCodeDosageFormMis, - "MIX" to kbvCodeDosageFormMix, - "MRG" to kbvCodeDosageFormMrg, - "MRP" to kbvCodeDosageFormMrp, - "MTA" to kbvCodeDosageFormMta, - "MUW" to kbvCodeDosageFormMuw, - "NAG" to kbvCodeDosageFormNag, - "NAO" to kbvCodeDosageFormNao, - "NAS" to kbvCodeDosageFormNas, - "NAW" to kbvCodeDosageFormNaw, - "NDS" to kbvCodeDosageFormNds, - "NSA" to kbvCodeDosageFormNsa, - "NTR" to kbvCodeDosageFormNtr, - "OCU" to kbvCodeDosageFormOcu, - "OEL" to kbvCodeDosageFormOel, - "OHT" to kbvCodeDosageFormOht, - "OVU" to kbvCodeDosageFormOvu, - "PAM" to kbvCodeDosageFormPam, - "PAS" to kbvCodeDosageFormPas, - "PEL" to kbvCodeDosageFormPel, - "PEN" to kbvCodeDosageFormPen, - "PER" to kbvCodeDosageFormPer, - "PFL" to kbvCodeDosageFormPfl, - "PFT" to kbvCodeDosageFormPft, - "PHI" to kbvCodeDosageFormPhi, - "PHV" to kbvCodeDosageFormPhv, - "PIE" to kbvCodeDosageFormPie, - "PIF" to kbvCodeDosageFormPif, - "PII" to kbvCodeDosageFormPii, - "PIJ" to kbvCodeDosageFormPij, - "PIK" to kbvCodeDosageFormPik, - "PIS" to kbvCodeDosageFormPis, - "PIV" to kbvCodeDosageFormPiv, - "PKI" to kbvCodeDosageFormPki, - "PLE" to kbvCodeDosageFormPle, - "PLF" to kbvCodeDosageFormPlf, - "PLG" to kbvCodeDosageFormPlg, - "PLH" to kbvCodeDosageFormPlh, - "PLI" to kbvCodeDosageFormPli, - "PLK" to kbvCodeDosageFormPlk, - "PLS" to kbvCodeDosageFormPls, - "PLV" to kbvCodeDosageFormPlv, - "PPL" to kbvCodeDosageFormPpl, - "PRS" to kbvCodeDosageFormPrs, - "PSE" to kbvCodeDosageFormPse, - "PST" to kbvCodeDosageFormPst, - "PUD" to kbvCodeDosageFormPud, - "PUL" to kbvCodeDosageFormPul, - "RED" to kbvCodeDosageFormRed, - "REK" to kbvCodeDosageFormRek, - "RET" to kbvCodeDosageFormRet, - "RGR" to kbvCodeDosageFormRgr, - "RKA" to kbvCodeDosageFormRka, - "RMS" to kbvCodeDosageFormRms, - "RSC" to kbvCodeDosageFormRsc, - "RSU" to kbvCodeDosageFormRsu, - "RUT" to kbvCodeDosageFormRut, - "SAF" to kbvCodeDosageFormSaf, - "SAL" to kbvCodeDosageFormSal, - "SAM" to kbvCodeDosageFormSam, - "SCH" to kbvCodeDosageFormSch, - "SEI" to kbvCodeDosageFormSei, - "SHA" to kbvCodeDosageFormSha, - "SIR" to kbvCodeDosageFormSir, - "SLZ" to kbvCodeDosageFormSlz, - "SMF" to kbvCodeDosageFormSmf, - "SMT" to kbvCodeDosageFormSmt, - "SMU" to kbvCodeDosageFormSmu, - "SPA" to kbvCodeDosageFormSpa, - "SPF" to kbvCodeDosageFormSpf, - "SPL" to kbvCodeDosageFormSpl, - "SPR" to kbvCodeDosageFormSpr, - "SPT" to kbvCodeDosageFormSpt, - "SRI" to kbvCodeDosageFormSri, - "SSU" to kbvCodeDosageFormSsu, - "STA" to kbvCodeDosageFormSta, - "STB" to kbvCodeDosageFormStb, - "STI" to kbvCodeDosageFormSti, - "STR" to kbvCodeDosageFormStr, - "SUB" to kbvCodeDosageFormSub, - "SUE" to kbvCodeDosageFormSue, - "SUL" to kbvCodeDosageFormSul, - "SUP" to kbvCodeDosageFormSup, - "SUS" to kbvCodeDosageFormSus, - "SUT" to kbvCodeDosageFormSut, - "SUV" to kbvCodeDosageFormSuv, - "SWA" to kbvCodeDosageFormSwa, - "TAB" to kbvCodeDosageFormTab, - "TAE" to kbvCodeDosageFormTae, - "TAM" to kbvCodeDosageFormTam, - "TEE" to kbvCodeDosageFormTee, - "TEI" to kbvCodeDosageFormTei, - "TES" to kbvCodeDosageFormTes, - "TIN" to kbvCodeDosageFormTin, - "TKA" to kbvCodeDosageFormTka, - "TLE" to kbvCodeDosageFormTle, - "TMR" to kbvCodeDosageFormTmr, - "TON" to kbvCodeDosageFormTon, - "TPN" to kbvCodeDosageFormTpn, - "TPO" to kbvCodeDosageFormTpo, - "TRA" to kbvCodeDosageFormTra, - "TRI" to kbvCodeDosageFormTri, - "TRO" to kbvCodeDosageFormTro, - "TRS" to kbvCodeDosageFormTrs, - "TRT" to kbvCodeDosageFormTrt, - "TSA" to kbvCodeDosageFormTsa, - "TSD" to kbvCodeDosageFormTsd, - "TSE" to kbvCodeDosageFormTse, - "TSS" to kbvCodeDosageFormTss, - "TST" to kbvCodeDosageFormTst, - "TSY" to kbvCodeDosageFormTsy, - "TTR" to kbvCodeDosageFormTtr, - "TUB" to kbvCodeDosageFormTub, - "TUE" to kbvCodeDosageFormTue, - "TUP" to kbvCodeDosageFormTup, - "TVW" to kbvCodeDosageFormTvw, - "UTA" to kbvCodeDosageFormUta, - "VAL" to kbvCodeDosageFormVal, - "VAR" to kbvCodeDosageFormVar, - "VCR" to kbvCodeDosageFormVcr, - "VER" to kbvCodeDosageFormVer, - "VGE" to kbvCodeDosageFormVge, - "VKA" to kbvCodeDosageFormVka, - "VLI" to kbvCodeDosageFormVli, - "VOV" to kbvCodeDosageFormVov, - "VST" to kbvCodeDosageFormVst, - "VSU" to kbvCodeDosageFormVsu, - "VTA" to kbvCodeDosageFormVta, - "WAT" to kbvCodeDosageFormWat, - "WGA" to kbvCodeDosageFormWga, - "WKA" to kbvCodeDosageFormWka, - "WKM" to kbvCodeDosageFormWkm, - "WUE" to kbvCodeDosageFormWue, - "XDG" to kbvCodeDosageFormXdg, - "XDS" to kbvCodeDosageFormXds, - "XFE" to kbvCodeDosageFormXfe, - "XGM" to kbvCodeDosageFormXgm, - "XHA" to kbvCodeDosageFormXha, - "XHS" to kbvCodeDosageFormXhs, - "XNC" to kbvCodeDosageFormXnc, - "XPK" to kbvCodeDosageFormXpk, - "XTC" to kbvCodeDosageFormXtc, - "ZAM" to kbvCodeDosageFormZam, - "ZBU" to kbvCodeDosageFormZbu, - "ZCR" to kbvCodeDosageFormZcr, - "ZGE" to kbvCodeDosageFormZge, - "ZKA" to kbvCodeDosageFormZka, - "ZPA" to kbvCodeDosageFormZpa -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt deleted file mode 100644 index 6e5c5409..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/IdpService.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.api - -import de.gematik.ti.erp.app.idp.api.models.Challenge -import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey -import de.gematik.ti.erp.app.idp.api.models.TokenResponse -import de.gematik.ti.erp.app.idp.repository.JWSDiscoveryDocument -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.http.Field -import retrofit2.http.FormUrlEncoded -import retrofit2.http.GET -import retrofit2.http.Headers -import retrofit2.http.POST -import retrofit2.http.Query -import retrofit2.http.Url -import java.net.URI - -const val REDIRECT_URI = "https://redirect.gematik.de/erezept" -const val CLIENT_ID = "eRezeptApp" - -interface IdpService { - - @GET("openid-configuration") - suspend fun discoveryDocument(): Response - - @GET - suspend fun idpPukSig( - @Url url: String - ): Response - - @GET - suspend fun idpPukEnc( - @Url url: String - ): Response - - @GET - suspend fun fetchTokenChallenge( - @Url url: String, - @Query("client_id") clientId: String = CLIENT_ID, - @Query("response_type") responseType: String = "code", - @Query("redirect_uri") redirect_uri: String = REDIRECT_URI, - @Query("state") state: String, - @Query("code_challenge") codeChallenge: String, - @Query("code_challenge_method") codeChallengeMethod: String = "S256", - @Query("scope") scope: String, - @Query("nonce") nonce: String - ): Response - - @FormUrlEncoded - @POST - @Headers( - "Accept: application/json" - ) - suspend fun authorization( - @Url url: String, - @Field("signed_challenge") signedChallenge: String - ): Response - - @FormUrlEncoded - @POST - @Headers( - "Accept: application/json" - ) - suspend fun token( - @Url url: String, - @Field("key_verifier") keyVerifier: String, - @Field("code") code: String, - @Field("grant_type") grantType: String = "authorization_code", - @Field("redirect_uri") redirectUri: String = REDIRECT_URI, - @Field("client_id") clientId: String = CLIENT_ID - ): Response - - @FormUrlEncoded - @POST - @Headers( - "Accept: application/json" - ) - suspend fun ssoToken( - @Url url: String, - @Field("ssotoken") ssoToken: String, - @Field("unsigned_challenge") unsignedChallenge: String - ): Response - - companion object { - fun extractQueryParameter(location: URI, key: String): String { - return location.query - .split("&") - .map { - val (k, v) = it.split("=", limit = 2) - Pair(k, v) - } - .find { it.first == key }?.second ?: error("no parameter for key: $key") - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt deleted file mode 100644 index c2aa64ac..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/BasicData.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:UseSerializers(JWSSerializer::class) - -package de.gematik.ti.erp.app.idp.api.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers -import org.jose4j.jwk.JsonWebKey -import org.jose4j.jwk.PublicJsonWebKey -import org.jose4j.jws.JsonWebSignature - -@Serializable -data class IdpDiscoveryInfo( - @SerialName("authorization_endpoint") val authorizationURL: String, - @SerialName("sso_endpoint") val ssoURL: String, - @SerialName("token_endpoint") val tokenURL: String, - @SerialName("uri_pair") val pairingURL: String, - @SerialName("auth_pair_endpoint") val authenticationURL: String, - @SerialName("uri_puk_idp_enc") val uriPukIdpEnc: String, - @SerialName("uri_puk_idp_sig") val uriPukIdpSig: String, - @SerialName("exp") val expirationTime: Long, - @SerialName("iat") val issuedAt: Long -) - -@JvmInline -value class JWSPublicKey(val jws: PublicJsonWebKey) - -@JvmInline -value class JWSKey(val jws: JsonWebKey) - -data class JWSChallenge(val jws: JsonWebSignature, val raw: String) - -@Serializable -data class Challenge( - val challenge: JWSChallenge -) - -@Serializable -data class TokenResponse( - @SerialName("access_token") val accessToken: String, - @SerialName("expires_in") val expiresIn: Long, - @SerialName("id_token") val idToken: String, - @SerialName("sso_token") val ssoToken: String? = null, - @SerialName("token_type") val tokenType: String -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt deleted file mode 100644 index 505e32ac..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/api/models/Serializers.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.api.models - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import org.jose4j.jws.JsonWebSignature -import org.jose4j.jwx.JsonWebStructure - -object JWSSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JWSSerializer", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: JWSChallenge) = error("not implemented") - override fun deserialize(decoder: Decoder): JWSChallenge { - val jws = decoder.decodeString() - return JWSChallenge(JsonWebStructure.fromCompactSerialization(jws) as JsonWebSignature, jws) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/di/IdpModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/di/IdpModule.kt deleted file mode 100644 index 5154bd39..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/di/IdpModule.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.di - -import de.gematik.ti.erp.app.cardwall.AuthenticationUseCase -import de.gematik.ti.erp.app.idp.repository.IdpLocalDataSource -import de.gematik.ti.erp.app.idp.repository.IdpRemoteDataSource -import de.gematik.ti.erp.app.idp.repository.IdpRepository -import de.gematik.ti.erp.app.idp.usecase.IdpBasicUseCase -import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindings.Scope -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton - -fun idpModule(scope: Scope) = DI.Module("IDP Module") { - bind { scoped(scope).singleton { IdpLocalDataSource() } } - bind { scoped(scope).singleton { IdpRemoteDataSource(instance()) } } - bind { scoped(scope).singleton { IdpRepository(instance(), instance(), instance()) } } - bind { scoped(scope).singleton { IdpBasicUseCase(instance(), instance()) } } - bind { scoped(scope).singleton { IdpUseCase(instance(), instance()) } } - bind { scoped(scope).singleton { AuthenticationUseCase(instance()) } } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt deleted file mode 100644 index 57d9713e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpLocalDataSource.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.repository - -import java.time.Instant -import org.bouncycastle.cert.X509CertificateHolder - -data class IdpConfiguration( - val authorizationEndpoint: String, - val ssoEndpoint: String, - val tokenEndpoint: String, - val pairingEndpoint: String, - val authenticationEndpoint: String, - val pukIdpEncEndpoint: String, - val pukIdpSigEndpoint: String, - val certificate: X509CertificateHolder, - val expirationTimestamp: Instant, - val issueTimestamp: Instant -) - -data class IdpAuthenticationDataEntity( - val singleSignOnToken: String? = null, - val singleSignOnTokenScope: SingleSignOnToken.Scope? = null, - - val healthCardCertificate: ByteArray? = null, - val aliasOfSecureElementEntry: ByteArray? = null -) - -class IdpLocalDataSource { - private var idpConfiguration: IdpConfiguration? = null - private var authData: IdpAuthenticationDataEntity? = null - - suspend fun saveIdpInfo(idpConfiguration: IdpConfiguration) { - this.idpConfiguration = idpConfiguration - } - - suspend fun loadIdpInfo(): IdpConfiguration? { - return idpConfiguration - } - - suspend fun clearIdpInfo() { - idpConfiguration = null - } - - suspend fun saveSingleSignOnToken(token: String?, scope: SingleSignOnToken.Scope?) { - authData = authData?.copy(singleSignOnToken = token, singleSignOnTokenScope = scope) - ?: IdpAuthenticationDataEntity(singleSignOnToken = token, singleSignOnTokenScope = scope) - } - - suspend fun saveSingleSignOnToken(token: String?) { - authData = authData?.copy(singleSignOnToken = token) ?: IdpAuthenticationDataEntity(singleSignOnToken = token) - } - - suspend fun loadIdpAuthData(): IdpAuthenticationDataEntity { - return authData ?: IdpAuthenticationDataEntity() - } - - suspend fun clearIdpAuthData() { - authData = null - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt deleted file mode 100644 index 28c3caf1..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.repository - -import de.gematik.ti.erp.app.core.ApiCallException -import de.gematik.ti.erp.app.core.safeApiCall -import de.gematik.ti.erp.app.core.safeApiCallRaw -import de.gematik.ti.erp.app.idp.api.IdpService -import okhttp3.ResponseBody -import retrofit2.Response -import java.net.HttpURLConnection - -private const val defaultScope = "e-rezept openid" -private const val pairingScope = "pairing openid" - -class IdpRemoteDataSource( - private val service: IdpService -) { - - suspend fun fetchDiscoveryDocument() = - safeApiCall("error loading discovery document") { service.discoveryDocument() } - - suspend fun fetchIdpPukSig(url: String) = - safeApiCall("error fetching idpPucSig") { service.idpPukSig(url) } - - suspend fun fetchIdpPukEnc(url: String) = - safeApiCall("error fetching idpPucEnc") { service.idpPukEnc(url) } - - suspend fun fetchChallenge( - url: String, - codeChallenge: String, - state: String, - nonce: String, - isDeviceRegistration: Boolean - ) = - safeApiCall("error loading challenge") { - service.fetchTokenChallenge( - url, - codeChallenge = codeChallenge, - state = state, - nonce = nonce, - scope = if (isDeviceRegistration) pairingScope else defaultScope - ) - } - - suspend fun postChallenge(url: String, signedChallenge: String) = - postToEndpointExpectingLocationRedirect { service.authorization(url, signedChallenge) } - - suspend fun postChallenge(url: String, ssoToken: String, unsignedChallenge: String) = - postToEndpointExpectingLocationRedirect { service.ssoToken(url, ssoToken, unsignedChallenge) } - - private suspend inline fun postToEndpointExpectingLocationRedirect( - crossinline call: suspend () -> Response - ) = - safeApiCallRaw("error posting to redirecting endpoint") { - val response = call() - if (response.code() == HttpURLConnection.HTTP_MOVED_TEMP) { - val headers = response.headers() - val location = requireNotNull(headers["Location"]) - - Result.success(location) - } else { - Result.failure( - ApiCallException("Expected redirect ${response.code()} ${response.message()}", response) - ) - } - } - - suspend fun postToken( - url: String, - keyVerifier: String, - code: String - ) = safeApiCall("error posting for token") { - service.token( - url = url, - keyVerifier = keyVerifier, - code = code - ) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt deleted file mode 100644 index e99d5e52..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.repository - -import de.gematik.ti.erp.app.idp.api.models.Challenge -import de.gematik.ti.erp.app.idp.api.models.IdpDiscoveryInfo -import de.gematik.ti.erp.app.vau.extractECPublicKey -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import org.bouncycastle.cert.X509CertificateHolder -import org.jose4j.base64url.Base64 -import org.jose4j.jws.JsonWebSignature -import java.time.Instant - -@JvmInline -value class JWSDiscoveryDocument(val jws: JsonWebSignature) - -data class SingleSignOnToken(val token: String, val scope: Scope = Scope.Default) { - enum class Scope { - Default, - AlternateAuthentication - } -} - -class IdpRepository( - private val remoteDataSource: IdpRemoteDataSource, - private val localDataSource: IdpLocalDataSource, - private val json: Json -) { - var decryptedAccessToken: String? = null - set(v) { - field = if (v?.isBlank() == true) null else v - } - - private var cardAccessNumber: String? = null - - suspend fun getSingleSignOnToken() = localDataSource.loadIdpAuthData().let { entity -> - entity.singleSignOnToken?.let { token -> - entity.singleSignOnTokenScope?.let { scope -> - SingleSignOnToken(token, scope) - } - } - } - - suspend fun setSingleSignOnToken(token: SingleSignOnToken) = - localDataSource.saveSingleSignOnToken(token.token, token.scope) - - suspend fun getSingleSignOnTokenScope() = - localDataSource.loadIdpAuthData().singleSignOnTokenScope - - suspend fun fetchChallenge( - url: String, - codeChallenge: String, - state: String, - nonce: String, - isDeviceRegistration: Boolean = false - ): Result = - remoteDataSource.fetchChallenge(url, codeChallenge, state, nonce, isDeviceRegistration) - - /** - * Returns an unchecked and possible invalid idp configuration parsed from the discovery document. - */ - suspend fun loadUncheckedIdpConfiguration(): IdpConfiguration { - return localDataSource.loadIdpInfo() ?: run { - extractUncheckedIdpConfiguration( - remoteDataSource.fetchDiscoveryDocument().getOrThrow() - ).also { localDataSource.saveIdpInfo(it) } - } - } - - suspend fun postSignedChallenge(url: String, signedChallenge: String): Result = - remoteDataSource.postChallenge(url, signedChallenge) - - suspend fun postUnsignedChallengeWithSso( - url: String, - ssoToken: String, - unsignedChallenge: String - ): Result = - remoteDataSource.postChallenge(url, ssoToken, unsignedChallenge) - - suspend fun postToken( - url: String, - keyVerifier: String, - code: String - ) = - remoteDataSource.postToken( - url, - keyVerifier = keyVerifier, - code = code - ) - - suspend fun fetchIdpPukSig(url: String) = - remoteDataSource.fetchIdpPukSig(url) - - suspend fun fetchIdpPukEnc(url: String) = - remoteDataSource.fetchIdpPukEnc(url) - - @OptIn(ExperimentalSerializationApi::class) - private fun parseDiscoveryDocumentBody(body: String): IdpDiscoveryInfo = - requireNotNull(json.decodeFromString(body)) { "Couldn't parse discovery document" } - - fun extractUncheckedIdpConfiguration(discoveryDocument: JWSDiscoveryDocument): IdpConfiguration { - val x5c = requireNotNull( - (discoveryDocument.jws.headers?.getObjectHeaderValue("x5c") as? ArrayList<*>)?.firstOrNull() as? String - ) { "Missing certificate" } - val certificateHolder = X509CertificateHolder(Base64.decode(x5c)) - - discoveryDocument.jws.key = certificateHolder.extractECPublicKey() - - val discoveryDocumentBody = parseDiscoveryDocumentBody(discoveryDocument.jws.payload) - - return IdpConfiguration( - authorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.authorizationURL), - ssoEndpoint = overwriteEndpoint(discoveryDocumentBody.ssoURL), - tokenEndpoint = overwriteEndpoint(discoveryDocumentBody.tokenURL), - pairingEndpoint = discoveryDocumentBody.pairingURL, - authenticationEndpoint = overwriteEndpoint(discoveryDocumentBody.authenticationURL), - pukIdpEncEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpEnc), - pukIdpSigEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpSig), - expirationTimestamp = convertTimeStampTo(discoveryDocumentBody.expirationTime), - issueTimestamp = convertTimeStampTo(discoveryDocumentBody.issuedAt), - certificate = certificateHolder - ) - } - - private fun convertTimeStampTo(timeStamp: Long) = - Instant.ofEpochSecond(timeStamp) - - private fun overwriteEndpoint(oldEndpoint: String) = - oldEndpoint.replace(".zentral.idp.splitdns.ti-dienste.de", ".app.ti-dienste.de") - - suspend fun invalidate() { - invalidateConfig() - invalidateDecryptedAccessToken() - localDataSource.clearIdpAuthData() - } - - suspend fun invalidateConfig() { - localDataSource.clearIdpInfo() - } - - suspend fun invalidateWithUserCredentials() { - invalidate() - - cardAccessNumber = "" // TODO better handling - } - - suspend fun invalidateSingleSignOnTokenRetainingScope() = - localDataSource.saveSingleSignOnToken(null) - - fun invalidateDecryptedAccessToken() { - decryptedAccessToken = null - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt deleted file mode 100644 index 06de2730..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt +++ /dev/null @@ -1,534 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.usecase - -import de.gematik.ti.erp.app.BCProvider -import de.gematik.ti.erp.app.cardwall.AuthenticationUseCase -import de.gematik.ti.erp.app.idp.EllipticCurvesExtending -import de.gematik.ti.erp.app.idp.api.IdpService -import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey -import de.gematik.ti.erp.app.idp.api.models.TokenResponse -import de.gematik.ti.erp.app.idp.buildJsonWebSignatureWithHealthCard -import de.gematik.ti.erp.app.idp.buildKeyVerifier -import de.gematik.ti.erp.app.idp.repository.IdpConfiguration -import de.gematik.ti.erp.app.idp.repository.IdpRepository -import de.gematik.ti.erp.app.utils.generateRandomAES256Key -import de.gematik.ti.erp.app.utils.secureRandomInstance -import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase -import de.gematik.ti.erp.app.vau.usecase.checkIdpCertificate -import io.github.aakira.napier.Napier -import java.net.URI -import java.security.MessageDigest -import java.security.PublicKey -import java.security.SecureRandom -import java.security.Security -import java.security.interfaces.ECPublicKey -import javax.crypto.SecretKey -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import org.jose4j.base64url.Base64 -import org.jose4j.base64url.Base64Url -import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers -import org.jose4j.jwe.JsonWebEncryption -import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers -import org.jose4j.jws.JsonWebSignature -import org.jose4j.jwt.JwtClaims -import org.jose4j.jwt.NumericDate -import org.jose4j.jwt.consumer.JwtContext -import org.jose4j.jwt.consumer.NumericDateValidator -import org.jose4j.jwx.JsonWebStructure -import kotlin.time.Duration.Companion.hours - -private val discoveryDocumentMaxValidityMinutes: Int = 24.hours.inWholeMinutes.toInt() -private val discoveryDocumentMaxValiditySeconds: Int = 24.hours.inWholeSeconds.toInt() - -enum class IdpScope { - Default, - BiometricPairing -} - -data class IdpChallengeFlowResult( - val scope: IdpScope, - val challenge: IdpUnsignedChallenge -) - -data class IdpAuthFlowResult( - val accessToken: String, - val ssoToken: String -) - -data class IdpRefreshFlowResult( - val scope: IdpScope, - val accessToken: String -) - -data class IdpInitialData( - val config: IdpConfiguration, - val pukSigKey: JWSPublicKey, - val pukEncKey: JWSPublicKey, - val state: IdpState, - val nonce: IdpNonce, - val codeVerifier: String, - val codeChallenge: String -) - -data class IdpUnsignedChallenge( - val signedChallenge: String, // raw jws - val challenge: String, // payload extracted from the jws - val expires: Long // expiry timestamp parsed from challenge -) - -@JvmInline -value class IdpState(val state: String) { - operator fun component1(): String = state - - companion object { - fun create(outLength: Int = 32) = IdpState(generateRandomUrlSafeStringSecure(outLength)) - } -} - -@JvmInline -value class IdpNonce(val nonce: String) { - operator fun component1(): String = nonce - - companion object { - fun create() = IdpNonce(generateRandomUrlSafeStringSecure(32)) - } -} - -internal fun generateRandomUrlSafeStringSecure(outLength: Int = 32): String { - require(outLength >= 1) - val chars = Base64Url.encode( - ByteArray((outLength / 4 + 1) * 3).apply { - SecureRandom().nextBytes(this) - } - ) - return chars.substring(0 until outLength) -} - -// -// Flow with health card: -// (1) [initializeConfigurationAndKeys] -> [challengeFlow] -> [basicAuthFlow] -> result: [accessToken] & [singleSignOnToken] -// -// Flow with health card & alternative authentication: -// (1) [secureElementKeyMaterial] -------- + -// \ -// [initializeConfigurationAndKeys] -> [challengeFlow] -> [basicAuthFlow] -> result: [accessToken] -> + [registerDeviceWithHealthCard] -> result: list of paired devices -// (2) [initializeConfigurationAndKeys] -> [challengeFlow] -> [authenticateWithSecureElement] -> result: [accessToken] & [singleSignOnToken] -// -// -// -// [initializeConfigurationAndKeys]: -// (1) load idp config (local or remote) -> check if it's still valid & check cert against truststore -// (2) fetch public signature key as JWK -> check cert against truststore & check x & y coordinates against included cert -// (3) fetch public encryption key as JWK -// (*) result: [idpConfig], [state] & [nonce], [codeVerifier], [codeChallenge], [pukSigKey as JWK], [pukEncKey as JWK] -// -// [challengeFlow]: -// (1) [codeChallenge] -> fetch challenge with [codeChallenge] -> validate with [state] & [nonce] & [pukSigKey] -// (*) result: [unsignedChallenge (payload of JWS; signature verified)], [signedChallenge (raw challenge from backend)] -// -// [basicAuthFlow]: -// (1) [signatureHandleOfHealthCard] - + -// [certificateOfHealthCard] - + -// \ -// [signedChallenge] ---------------> + sign & encrypt -> [challenge as JWE(JWS(JWS))] -> post -> result: [redirect] -// -// (2) [code] ------------------- + -// [ephemeralSymmetricalKey] - + -// [codeVerifier] ------------- + -> [keyVerifier as JWE] -// | -// v -// (3) [redirect] -> extract [code] & [ssoToken] + -> post [code] with [keyVerifier] -> result: [encryptedAccessToken] -// (4) [encryptedAccessToken as JWE(JWS)] -> decrypt with [ephemeralSymmetricalKey] -> [accessToken as JWS] -> validate with [state] & [nonce] & [pukSigKey] -// (*) result: [accessToken as JWS] & [ssoToken] -// - -class IdpBasicUseCase( - private val repository: IdpRepository, - private val truststoreUseCase: TruststoreUseCase -) { - private val seed = AuthenticationUseCase.Companion.seed - // //////////////////////////////////////////////////////////////////////////////////////// - - suspend fun initializeConfigurationAndKeys(): IdpInitialData { - cryptoInitializedLock.withLock { - if (!isCryptoInitialized) { - Security.removeProvider("BC") - Security.insertProviderAt(BCProvider, 1) - EllipticCurvesExtending.init() - isCryptoInitialized = true - } - } - - val config = try { - repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Clock.System.now()) - } - } catch (e: Exception) { - Napier.e("IDP config couldn't be validated", e) - repository.invalidateConfig() - // retry - try { - repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Clock.System.now()) - } - } catch (e: Exception) { - Napier.e("IDP config couldn't be validated again; finally aborting", e) - repository.invalidateConfig() - throw e - } - } - - // fetch both keys - val pukSigKey = repository.fetchIdpPukSig(config.pukIdpSigEndpoint).getOrThrow() - val pukEncKey = repository.fetchIdpPukEnc(config.pukIdpEncEndpoint).getOrThrow() - - // check signature key with truststore - val certPubKey = pukSigKey.jws.leafCertificate.publicKey as ECPublicKey - val pubKey = pukSigKey.jws.publicKey as ECPublicKey - require(certPubKey.w == pubKey.w) { "Public key of idpPukSig doesn't match with its contained certificate" } - truststoreUseCase.checkIdpCertificate(pukSigKey.jws.leafCertificate) - - // generate state & nonce used to verify the integrity of certain calls - val state = IdpState.create() - val nonce = IdpNonce.create() - - val codeVerifier = generateCodeVerifier() - val codeChallenge = generateCodeChallenge(codeVerifier) - - return IdpInitialData( - config, - pukSigKey = pukSigKey, - pukEncKey = pukEncKey, - state = state, - nonce = nonce, - codeVerifier = codeVerifier, - codeChallenge = codeChallenge - ) - } - - suspend fun challengeFlow( - initialData: IdpInitialData, - scope: IdpScope - ): IdpChallengeFlowResult { - val (config, pukSigKey, _, state, nonce) = initialData - val codeChallenge = initialData.codeChallenge - - // fetch and check the challenge - - val challenge = fetchAndCheckUnsignedChallenge( - url = config.authorizationEndpoint, - codeChallenge = codeChallenge, - state = state, - nonce = nonce, - scope = scope, - pukSigKey = pukSigKey - ) - - return IdpChallengeFlowResult( - scope = scope, - challenge = challenge - ) - } - - suspend fun basicAuthFlow( - initialData: IdpInitialData, - challengeData: IdpChallengeFlowResult, - healthCardCertificate: ByteArray, - sign: suspend (hash: ByteArray) -> ByteArray - ): IdpAuthFlowResult { - val (config, pukSigKey, pukEncKey, state, nonce) = initialData - val codeVerifier = initialData.codeVerifier - - // sign [challengeBody] with the health card - - val signedChallenge = buildSignedChallenge(challengeData.challenge.signedChallenge, healthCardCertificate) { - sign(it) - } - val encryptedSignedChallenge = - buildEncryptedSignedChallenge(signedChallenge, challengeData.challenge.expires, pukEncKey.jws.publicKey) - - // post encrypted signed challenge & parse returned redirect url - - val redirect = postSignedChallengeAndGetRedirect( - config.authorizationEndpoint, - codeChallenge = encryptedSignedChallenge, - state = state - ) - - val redirectCodeJwe = IdpService.extractQueryParameter(redirect, "code") - val redirectSsoToken = IdpService.extractQueryParameter(redirect, "ssotoken") - - // post [redirectCodeJwe] &b get the access token - - val accessToken = postCodeAndDecryptAccessToken( - config.tokenEndpoint, - nonce = nonce, - codeVerifier = codeVerifier, - code = redirectCodeJwe, - pukEncKey = pukEncKey, - pukSigKey = pukSigKey - ) - - // final [redirectSsoToken] & [accessToken] - return IdpAuthFlowResult( - accessToken = accessToken, - ssoToken = redirectSsoToken - ) - } - - suspend fun refreshAccessTokenWithSsoFlow( - initialData: IdpInitialData, - scope: IdpScope, - ssoToken: String - ): IdpRefreshFlowResult { - val (config, pukSigKey, pukEncKey) = initialData - val state = initialData.state - val nonce = initialData.nonce - val codeVerifier = initialData.codeVerifier - val codeChallenge = initialData.codeChallenge - - val unsignedChallenge = fetchAndCheckUnsignedChallenge( - url = config.authorizationEndpoint, - codeChallenge = codeChallenge, - state = state, - nonce = nonce, - scope = scope, - pukSigKey = pukSigKey - ) - - val redirect = postUnsignedChallengeWithSsoTokenAndGetRedirect( - config.ssoEndpoint, - // we post an unsigned (i.e., we didn't sign the challenge, which is signed by the idp, again) to the sso response endpoint - unsignedCodeChallenge = unsignedChallenge.signedChallenge, - ssoToken = ssoToken, - state = state - ) - - val codeFromRedirect = IdpService.extractQueryParameter(redirect, "code") - - val accessToken = postCodeAndDecryptAccessToken( - config.tokenEndpoint, - nonce = nonce, - codeVerifier = codeVerifier, - code = codeFromRedirect, - pukEncKey = pukEncKey, - pukSigKey = pukSigKey - ) - - return IdpRefreshFlowResult(scope, accessToken) - } - - // //////////////////////////////////////////////////////////////////////////////////////// - - suspend fun postSignedChallengeAndGetRedirect( - url: String, - codeChallenge: JsonWebEncryption, - state: IdpState - ): URI { - val redirect = - URI(repository.postSignedChallenge(url, codeChallenge.compactSerialization).getOrThrow()) - - val redirectState = IdpService.extractQueryParameter(redirect, "state") - require(state.state == redirectState) { "Invalid state" } - - return redirect - } - - suspend fun postUnsignedChallengeWithSsoTokenAndGetRedirect( - url: String, - unsignedCodeChallenge: String, - ssoToken: String, - state: IdpState - ): URI { - val redirect = - URI(repository.postUnsignedChallengeWithSso(url, ssoToken, unsignedCodeChallenge).getOrThrow()) - - val redirectState = IdpService.extractQueryParameter(redirect, "state") - require(state.state == redirectState) { "Invalid state" } - - return redirect - } - suspend fun fetchAndCheckUnsignedChallenge( - url: String, - codeChallenge: String, - state: IdpState, - nonce: IdpNonce, - scope: IdpScope, - pukSigKey: JWSPublicKey - ): IdpUnsignedChallenge { - val signedChallenge = repository.fetchChallenge( - url = url, - codeChallenge = codeChallenge, - state = state.state, - nonce = nonce.nonce, - isDeviceRegistration = scope == IdpScope.BiometricPairing - ).getOrThrow().let { - it.challenge.jws.apply { - key = pukSigKey.jws.publicKey - } - it.challenge - } - - // check state & nonce - val unsignedChallenge = signedChallenge.jws.payload - val unsignedChallengeJson = Json.parseToJsonElement(unsignedChallenge) - require(state.state == unsignedChallengeJson.jsonObject["state"]!!.jsonPrimitive.content) { "Invalid state" } - require(nonce.nonce == unsignedChallengeJson.jsonObject["nonce"]!!.jsonPrimitive.content) { "Invalid nonce" } - - val unsignedChallengeExpires = unsignedChallengeJson.jsonObject["exp"]!!.jsonPrimitive.long - - return IdpUnsignedChallenge(signedChallenge.raw, unsignedChallenge, unsignedChallengeExpires) - } - - suspend fun postCodeAndDecryptAccessToken( - url: String, - nonce: IdpNonce, - codeVerifier: String, - code: String, - pukEncKey: JWSPublicKey, - pukSigKey: JWSPublicKey - ): String { - val symmetricalKey = generateRandomAES256Key(seed.value) - - val keyVerifier = buildKeyVerifier( - symmetricalKey, - codeVerifier, - pukEncKey.jws.publicKey - ) - - return repository.postToken(url, keyVerifier.compactSerialization, code).getOrThrow().let { - checkNonce(decryptIdToken(it, symmetricalKey), pukSigKey.jws.publicKey, nonce.nonce) - - val json = decryptAccessToken(it, symmetricalKey) - Json.parseToJsonElement(json).jsonObject["njwt"]!!.jsonPrimitive.content - } - } - suspend fun buildSignedChallenge( - challengeBody: String, - healthCardCertificate: ByteArray, - sign: suspend (hash: ByteArray) -> ByteArray - ): String = - buildJsonWebSignatureWithHealthCard( - { - contentTypeHeaderValue = "NJWT" - algorithmHeaderValue = "BP256R1" - setHeader("x5c", listOf(Base64.encode(healthCardCertificate))) - payload = """{ "njwt": "$challengeBody" }""" - }, - sign - ) - - fun buildEncryptedSignedChallenge( - signedChallenge: String, - challengeExpires: Long, - idpPukEncKey: PublicKey - ) = - JsonWebEncryption().apply { - contentTypeHeaderValue = "NJWT" - algorithmHeaderValue = KeyManagementAlgorithmIdentifiers.ECDH_ES - encryptionMethodHeaderParameter = ContentEncryptionAlgorithmIdentifiers.AES_256_GCM - setHeader("exp", challengeExpires) - key = idpPukEncKey - payload = """{ "njwt": "$signedChallenge" }""" - } - - fun generateCodeVerifier(): String { - // https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 - // 60 bytes are about 80 characters of base64 - return Base64Url.encode( - ByteArray(60).apply { - secureRandomInstance(seed.value).nextBytes(this) - } - ) - } - - fun generateCodeChallenge(codeVerifier: String): String { - // https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 - return Base64Url.encode( - MessageDigest.getInstance("SHA-256").apply { - update(codeVerifier.toByteArray(Charsets.UTF_8)) - }.digest() - ) - } - - suspend fun checkIdpConfigurationValidity(config: IdpConfiguration, timestamp: Instant) { - truststoreUseCase.checkIdpCertificate(config.certificate) - - val claims = JwtClaims().apply { - issuedAt = NumericDate.fromMilliseconds(config.issueTimestamp.toEpochMilli()) - expirationTime = NumericDate.fromMilliseconds(config.expirationTimestamp.toEpochMilli()) - } - - val r = NumericDateValidator().apply { - setAllowedClockSkewSeconds(60) - setEvaluationTime(NumericDate.fromMilliseconds(timestamp.toEpochMilliseconds())) - setRequireExp(true) - setRequireIat(true) - setIatAllowedSecondsInThePast(discoveryDocumentMaxValiditySeconds) - setMaxFutureValidityInMinutes(discoveryDocumentMaxValidityMinutes) - }.validate(JwtContext(claims, emptyList())) - - require(r == null) { "IDP configuration couldn't be validated: ${r.errorMessage}" } - } - - /** - * Returns the actual id_token. - */ - private fun decryptIdToken(data: TokenResponse, key: SecretKey): JsonWebSignature { - val json = decryptJWE(data.idToken, key) - return JsonWebStructure.fromCompactSerialization( - Json.parseToJsonElement(json).jsonObject["njwt"]!!.jsonPrimitive.content - ) as JsonWebSignature - } - - /** - * Compares the contained nonce with [nonce] after checking the signature of the JWS. - */ - private fun checkNonce(idToken: JsonWebSignature, idpPukSigKey: PublicKey, nonce: String) { - require( - Json.parseToJsonElement( - idToken.apply { - key = idpPukSigKey - }.payload - ).jsonObject["nonce"]!!.jsonPrimitive.content == nonce - ) - } - - private fun decryptAccessToken(data: TokenResponse, symmetricalKey: SecretKey): String = - decryptJWE(data.accessToken, symmetricalKey) - - private fun decryptJWE(encryptedJWE: String, symmetricalKey: SecretKey): String = - JsonWebEncryption().apply { - key = symmetricalKey - compactSerialization = encryptedJWE - }.payload - - companion object { - private var isCryptoInitialized = false - private var cryptoInitializedLock = Mutex() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt deleted file mode 100644 index 9ffc5b56..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp.usecase - -import de.gematik.ti.erp.app.core.ApiCallException -import de.gematik.ti.erp.app.idp.repository.IdpRepository -import de.gematik.ti.erp.app.idp.repository.SingleSignOnToken -import io.github.aakira.napier.Napier -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.io.IOException - -/** - * Exception thrown by [IdpUseCase.loadAccessToken]. - */ -class RefreshFlowException : IOException { - /** - * Is true if the sso token is not valid anymore and the user is required to authenticate again. - */ - val userActionRequired: Boolean - val tokenScope: SingleSignOnToken.Scope? - - constructor(userActionRequired: Boolean, tokenScope: SingleSignOnToken.Scope?, cause: Throwable) : super(cause) { - this.userActionRequired = userActionRequired - this.tokenScope = tokenScope - } - - constructor(userActionRequired: Boolean, tokenScope: SingleSignOnToken.Scope?, message: String) : super(message) { - this.userActionRequired = userActionRequired - this.tokenScope = tokenScope - } -} - -class AltAuthenticationCryptoException(cause: Throwable) : IllegalStateException(cause) - -class IdpUseCase( - private val repository: IdpRepository, - private val basicUseCase: IdpBasicUseCase -) { - private val lock = Mutex() - - /** - * If no bearer token is set or [refresh] is true, this will trigger [IdpBasicUseCase.refreshAccessTokenWithSsoFlow]. - */ - suspend fun loadAccessToken(refresh: Boolean = false): String = - lock.withLock { - val ssoToken = repository.getSingleSignOnToken() - if (ssoToken == null) { - repository.invalidateDecryptedAccessToken() - throw RefreshFlowException( - true, - repository.getSingleSignOnTokenScope(), - "SSO token not set!" - ) - } - val accToken = repository.decryptedAccessToken - - if (refresh || accToken == null) { - repository.invalidateDecryptedAccessToken() - - val initialData = basicUseCase.initializeConfigurationAndKeys() - try { - val refreshData = basicUseCase.refreshAccessTokenWithSsoFlow( - initialData, - scope = IdpScope.Default, - ssoToken = ssoToken.token - ) - refreshData.accessToken - } catch (e: Exception) { - Napier.e("Couldn't refresh access token", e) - (e as? ApiCallException)?.also { - when (it.response.code()) { - // 400 returned by redirect call if sso token is not valid anymore - 400, 401, 403 -> { - repository.invalidateSingleSignOnTokenRetainingScope() - throw RefreshFlowException(true, ssoToken.scope, e) - } - } - } - throw RefreshFlowException(false, null, e) - } - } else { - accToken - }.also { - repository.decryptedAccessToken = it - } - } - - /** - * Initial flow fetching the sso & access token requiring the health card to sign the challenge. - */ - suspend fun authenticationFlowWithHealthCard( - healthCardCertificate: suspend () -> ByteArray, - sign: suspend (hash: ByteArray) -> ByteArray - ) = lock.withLock { - val initialData = basicUseCase.initializeConfigurationAndKeys() - val challengeData = basicUseCase.challengeFlow(initialData, scope = IdpScope.Default) - val basicData = basicUseCase.basicAuthFlow( - initialData = initialData, - challengeData = challengeData, - healthCardCertificate = healthCardCertificate(), - sign = sign - ) - repository.setSingleSignOnToken(SingleSignOnToken(basicData.ssoToken)) - repository.decryptedAccessToken = basicData.accessToken - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/CredentialsTextField.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/CredentialsTextField.kt deleted file mode 100644 index d1917e8b..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/CredentialsTextField.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.login.ui - -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.runtime.currentRecomposeScope -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun CredentialsTextField( - value: String, - valuePattern: Regex, - onValueChange: (String) -> Unit, - onEnter: () -> Unit, - modifier: Modifier = Modifier, - keyboardOptions: KeyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.NumberPassword, - imeAction = ImeAction.Next - ), - keyboardActions: KeyboardActions = KeyboardActions.Default, - decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit -) { - val textValue = TextFieldValue( - text = value, - selection = TextRange(value.length) - ) - - val scope = currentRecomposeScope - - BasicTextField( - value = textValue, - onValueChange = { - if (it.text.matches(valuePattern)) { - onValueChange(it.text) - } else { - // internal state of BasicTextField is otherwise not updated - scope.invalidate() - } - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = true, - modifier = modifier - .onPreviewKeyEvent { - if (it.key == Key.Enter) { - onEnter() - } - false - }, - decorationBox = decorationBox - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardScreen.kt deleted file mode 100644 index 3afa37fc..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardScreen.kt +++ /dev/null @@ -1,959 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.login.ui - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.with -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FloatingActionButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.contentColorFor -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Power -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.ArrowForward -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material.icons.rounded.Error -import androidx.compose.material.icons.rounded.RadioButtonUnchecked -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.scale -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.mouse.mouseScrollFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.constrainHeight -import androidx.compose.ui.unit.constrainWidth -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.cardwall.AuthenticationState -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.ClosablePopupScaffold -import de.gematik.ti.erp.app.common.SpacerLarge -import de.gematik.ti.erp.app.common.SpacerMedium -import de.gematik.ti.erp.app.common.SpacerSmall -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import java.util.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt - -@OptIn( - ExperimentalMaterialApi::class, - ExperimentalAnimationApi::class -) -@Composable -fun LoginWithHealthCard( - viewModel: LoginWithHealthCardViewModel, - onFinished: () -> Unit, - onClose: () -> Unit -) { - var privacyAndTermsToggled by remember { mutableStateOf(false) } - var cardReaderPresent by remember { mutableStateOf(false) } - var personalIdentificationNumber by remember { mutableStateOf("") } - var cardAccessNumber by remember { mutableStateOf("") } - - ClosablePopupScaffold(onClose = onClose) { - val state = rememberLazyListState() - Box { - var authState by remember { mutableStateOf(AuthenticationState.None) } - - var triggerAuth by remember { mutableStateOf(true) } - LaunchedEffect(state.firstVisibleItemIndex == 4, triggerAuth) { - if (triggerAuth && state.firstVisibleItemIndex == 4 && - cardAccessNumber.isNotBlank() && personalIdentificationNumber.isNotBlank() - ) { - viewModel.authenticate(can = cardAccessNumber, pin = personalIdentificationNumber) - .collect { - if (it.isFinal()) { - onFinished() - } else { - authState = it - } - - if (it.isFailure()) { - triggerAuth = false - } - } - } - } - - val isCardAccessNumberValid = cardAccessNumber.matches("""^\d{6}$""".toRegex()) - val isPersonalIdentificationNumberValid = personalIdentificationNumber.matches("""^\d{6,8}$""".toRegex()) - - val maxPages = - if (privacyAndTermsToggled) { - if (cardReaderPresent) { - if (isCardAccessNumberValid) { - if (isPersonalIdentificationNumberValid) 5 - else 4 - } else 3 - } else 2 - } else 1 - - val scope = rememberCoroutineScope() - - val onNextPage = { - scope.launch { state.animateScrollToItem(state.firstVisibleItemIndex + 1) } - } - - Column(Modifier.fillMaxSize()) { - LazyRow( - modifier = Modifier.weight(1f), - state = state - ) { - items(count = maxPages) { page -> - when (page) { - 0 -> PageContainer(App.strings.desktopLoginPageDataTerms()) { - PrivacyAndTerms( - toggled = privacyAndTermsToggled, - onPrivacyAndTermsToggled = { - privacyAndTermsToggled = it - } - ) - } - - 1 -> PageContainer(App.strings.desktopLoginPageConnectReader()) { - AwaitCardReader( - viewModel, - onCardReaderPresent = { - cardReaderPresent = true - } - ) - } - - 2 -> PageContainer(App.strings.desktopLoginPageEnterCan()) { - EnterCardAccessNumber( - can = cardAccessNumber, - onCanChange = { cardAccessNumber = it }, - onEnter = { - if (isCardAccessNumberValid) { - onNextPage() - } - } - ) - } - - 3 -> PageContainer(App.strings.desktopLoginPageEnterPin()) { - EnterPersonalIdentificationNumber( - pin = personalIdentificationNumber, - onPinChange = { personalIdentificationNumber = it }, - onEnter = { - if (isPersonalIdentificationNumberValid) { - onNextPage() - } - } - ) - } - - 4 -> PageContainer(App.strings.desktopLoginPageConnectHealthcard()) { - ReadHealthCardAndDownloadData( - authState, - onReenterCan = { - scope.launch { - state.animateScrollToItem(2) - triggerAuth = true - } - }, - onReenterPin = { - scope.launch { - state.animateScrollToItem(3) - triggerAuth = true - } - }, - onRetry = { triggerAuth = true }, - onSuccess = { - println("CAN $cardAccessNumber") - Arrays.fill(cardAccessNumber.toCharArray(), '\u0000') - println("CAN nulled $cardAccessNumber") - } - ) - } - } - } - } - - PageIndicator(modifier = Modifier.align(Alignment.CenterHorizontally), listState = state, 5) - } - - if (state.firstVisibleItemIndex > 0) { - NavigationBack( - modifier = Modifier.align(Alignment.BottomStart) - .padding(start = PaddingDefaults.Large, bottom = 44.dp), - onClick = { - scope.launch { state.animateScrollToItem(state.firstVisibleItemIndex - 1) } - } - ) { - Icon(Icons.Rounded.ArrowBack, null) - Spacer(Modifier.size(8.dp)) - Text(App.strings.back()) - } - } - - NavigationForward( - modifier = Modifier.align(Alignment.BottomEnd).padding(end = PaddingDefaults.Large, bottom = 44.dp), - enabled = state.firstVisibleItemIndex < maxPages - 1, - onClick = { - onNextPage() - } - ) { - Icon(Icons.Rounded.ArrowForward, null) - } - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun LazyItemScope.PageContainer( - title: String, - content: @Composable () -> Unit -) { - Column { - Text( - title, - style = MaterialTheme.typography.h5, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - SpacerLarge() - Row( - Modifier - .mouseScrollFilter { event, bounds -> true } - .fillParentMaxSize() - .padding(horizontal = 124.dp) - ) { - val scrollState = rememberScrollState() - val scrollbarAdapter = rememberScrollbarAdapter(scrollState) - Box( - modifier = Modifier.fillMaxSize().verticalScroll(scrollState).weight(1f), - propagateMinConstraints = true - ) { - content() - } - VerticalScrollbar(scrollbarAdapter) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun NavigationForward( - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit, - content: @Composable RowScope.() -> Unit -) { - val backgroundColor = - if (enabled) { - MaterialTheme.colors.secondary - } else { - AppTheme.colors.neutral300 - } - - val contentColor = if (enabled) { - contentColorFor(backgroundColor) - } else { - AppTheme.colors.neutral500 - } - - val interactionSource = remember { MutableInteractionSource() } - rememberRipple() - Surface( - onClick = { - if (enabled) { - onClick() - } - }, - modifier = modifier, - enabled = enabled, - shape = CircleShape, - color = backgroundColor, - contentColor = contentColor, - elevation = if (enabled) FloatingActionButtonDefaults.elevation().elevation(interactionSource).value else 0.dp - ) { - CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { - ProvideTextStyle(MaterialTheme.typography.button) { - Box( - modifier = Modifier - .defaultMinSize(minWidth = 32.dp, minHeight = 32.dp) - .padding(8.dp), - contentAlignment = Alignment.Center - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - content() - } - } - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun NavigationBack( - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit, - content: @Composable RowScope.() -> Unit -) { - Surface( - onClick = { - if (enabled) { - onClick() - } - }, - modifier = modifier, - shape = CircleShape, - color = Color.Unspecified, - contentColor = AppTheme.colors.neutral600 - ) { - ProvideTextStyle(MaterialTheme.typography.button) { - Box( - modifier = Modifier - .defaultMinSize(minWidth = 32.dp, minHeight = 32.dp) - .padding(8.dp), - contentAlignment = Alignment.Center - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - content() - } - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun PageIndicator( - modifier: Modifier = Modifier, - listState: LazyListState, - maxPages: Int -) { - Box( - modifier = modifier - .padding(bottom = PaddingDefaults.Large) - .clip(CircleShape) - .background(AppTheme.colors.neutral100.copy(alpha = 0.5f)) - .padding(PaddingDefaults.Small) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - repeat(maxPages) { - Dot(color = AppTheme.colors.neutral300) - } - } - val pageSize = - listState.layoutInfo.visibleItemsInfo.find { it.index == listState.firstVisibleItemIndex }?.size ?: 1 - val fraction = - listState.firstVisibleItemIndex.toFloat() + (listState.firstVisibleItemScrollOffset / pageSize.toFloat()) - val offsetX = with(LocalDensity.current) { - val gap = PaddingDefaults.Small.roundToPx() - val size = 8.dp.roundToPx() - (fraction * (size + gap)).toDp() - } - Dot(modifier = Modifier.offset(x = offsetX), color = AppTheme.colors.primary500) - } -} - -@Composable -private fun Dot(modifier: Modifier = Modifier, color: Color) { - Box( - modifier = modifier - .clip(CircleShape) - .background(color) - .size(8.dp) - ) -} - -@Composable -private fun PrivacyAndTerms( - toggled: Boolean, - onPrivacyAndTermsToggled: (Boolean) -> Unit -) { - Column(Modifier.fillMaxSize()) { - Text( - text = App.strings.onBoardingPage4Info(), - style = MaterialTheme.typography.body1 - ) - - var privacyChecked by remember { mutableStateOf(toggled) } - var termsChecked by remember { mutableStateOf(toggled) } - - LaunchedEffect(privacyChecked, termsChecked) { - onPrivacyAndTermsToggled(privacyChecked && termsChecked) - } - - SpacerLarge() - - val uriHandler = LocalUriHandler.current - val dataUri = App.strings.desktopDataLink() - val termsUri = App.strings.desktopTermsLink() - Toggle( - text = App.strings.onBoardingPage4InfoAcceptInfo( - count = 1, - buildAnnotatedString { - pushStyle(SpanStyle(color = AppTheme.colors.primary500)) - append(App.strings.onBoardingPage4InfoDataprotection()) - pop() - } - ), - checked = privacyChecked, - onCheckedChange = { - privacyChecked = it - }, - onClickInfo = { - uriHandler.openUri(dataUri) - } - ) - SpacerMedium() - Toggle( - text = App.strings.onBoardingPage4InfoAcceptInfo( - count = 1, - buildAnnotatedString { - pushStyle(SpanStyle(color = AppTheme.colors.primary500)) - append(App.strings.onBoardingPage4InfoTos()) - pop() - } - ), - checked = termsChecked, - onCheckedChange = { - termsChecked = it - }, - onClickInfo = { - uriHandler.openUri(termsUri) - } - ) - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun Toggle( - text: AnnotatedString, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - onClickInfo: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = text, - style = MaterialTheme.typography.body1, - modifier = Modifier - .weight(1f) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onClickInfo - ) - ) - - SpacerMedium() - - AnimatedContent( - targetState = checked, - transitionSpec = { - fadeIn() with fadeOut() - }, - modifier = Modifier - .align(Alignment.CenterVertically) - .toggleable( - value = checked, - onValueChange = onCheckedChange, - role = Role.Checkbox, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( - bounded = false, - radius = PaddingDefaults.Large - ) - ), - contentAlignment = Alignment.Center - ) { - when (it) { - false -> Icon( - Icons.Rounded.RadioButtonUnchecked, - null, - tint = AppTheme.colors.neutral400 - ) - - true -> Icon( - Icons.Rounded.CheckCircle, - null, - tint = AppTheme.colors.primary600 - ) - } - } - } -} - -private enum class ReaderState { - Searching, Found, Error -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun AwaitCardReader( - viewModel: LoginWithHealthCardViewModel, - onCardReaderPresent: () -> Unit -) { - var readerState by remember { mutableStateOf(ReaderState.Searching) } - - LaunchedEffect(Unit) { - delay(1000) - viewModel.waitForAnyReader() - delay(1000) - onCardReaderPresent() - readerState = ReaderState.Found - } - - Box(Modifier.fillMaxSize().padding(PaddingDefaults.Medium), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - AnimatedContent(targetState = readerState, modifier = Modifier.size(72.dp), transitionSpec = { - when (readerState) { - ReaderState.Searching -> fadeIn() with fadeOut() - ReaderState.Found -> slideInVertically { height -> height } + fadeIn() with fadeOut() - ReaderState.Error -> fadeIn() with fadeOut() - } - }) { - when (it) { - ReaderState.Searching -> CircularProgressIndicator(color = AppTheme.colors.primary400) - ReaderState.Found -> Icon(Icons.Filled.Power, null, tint = AppTheme.colors.green400) - ReaderState.Error -> Icon(Icons.Filled.Error, null, tint = AppTheme.colors.red600) - } - } - SpacerMedium() - Text( - when (readerState) { - ReaderState.Searching -> App.strings.desktopLoginPageReaderSearchHealthcard() - ReaderState.Found -> App.strings.desktopLoginPageReaderFoundHealthcard() - ReaderState.Error -> App.strings.desktopLoginPageReaderError() - }, - style = MaterialTheme.typography.subtitle1, - textAlign = TextAlign.Center - ) - } - } -} - -@Composable -private fun EnterPersonalIdentificationNumber( - pin: String, - onPinChange: (String) -> Unit, - onEnter: () -> Unit -) { - Box(Modifier.fillMaxSize().padding(PaddingDefaults.Medium)) { - Column(Modifier.align(Alignment.Center).fillMaxWidth()) { - var isFocussed by remember { mutableStateOf(false) } - - CredentialsTextField( - value = pin, - valuePattern = """^\d{0,8}$""".toRegex(), - onValueChange = onPinChange, - onEnter = onEnter, - modifier = Modifier - .fillMaxWidth() - .padding( - start = PaddingDefaults.Medium, - bottom = PaddingDefaults.Small, - end = PaddingDefaults.Medium - ) - .onFocusChanged { - isFocussed = it.isFocused - } - ) { - Column(modifier = Modifier.fillMaxWidth()) { - val shape = RoundedCornerShape(8.dp) - val borderModifier = Modifier.border( - BorderStroke(1.dp, color = AppTheme.colors.primary700), - shape - ) - - Row( - modifier = Modifier - .heightIn(min = 48.dp) - .shadow(1.dp, shape) - .then(if (isFocussed) borderModifier else Modifier) - .background( - color = AppTheme.colors.neutral200, - shape = shape - ) - ) { - var pinVisible by remember { mutableStateOf(false) } - val transformedPin = if (pinVisible) { - pin - } else { - pin.asIterable().joinToString(separator = "") { "\u2B24" } - } - - val spacer = with(LocalDensity.current) { - 48.dp.roundToPx() - } - var textWidth by remember { mutableStateOf(1) } - Text( - text = transformedPin, - style = MaterialTheme.typography.h6, - letterSpacing = 0.7.em, - textAlign = TextAlign.Center, - softWrap = false, - overflow = TextOverflow.Visible, - onTextLayout = { res -> - textWidth = res.multiParagraph.getLineWidth(0).roundToInt() - }, - modifier = Modifier - .weight(1f) - .padding(start = 48.dp) - .wrapContentWidth() - .layout { measurable, constraints -> - val p = measurable.measure(constraints) - val width = constraints.constrainWidth(p.width) - val height = constraints.constrainHeight(p.height) - layout(width, height) { - val space = max(min(width - textWidth, 0), -spacer) - p.place(x = space, y = 0) - } - } - .align(Alignment.CenterVertically) - .clearAndSetSemantics { } - .drawWithContent { - val sW = min(1f, (spacer + size.width) / textWidth) - scale(sW, sW, pivot = Offset(x = 0f, y = center.y)) { - this@drawWithContent.drawContent() - } - } - ) - - IconToggleButton( - checked = pinVisible, - onCheckedChange = { pinVisible = it }, - modifier = Modifier.align(Alignment.CenterVertically) - ) { - when (pinVisible) { - true -> Icon( - Icons.Rounded.Visibility, - null, - tint = AppTheme.colors.neutral600 - ) - - false -> Icon( - Icons.Rounded.VisibilityOff, - null, - tint = AppTheme.colors.neutral600 - ) - } - } - } - Spacer(Modifier.size(16.dp)) - Text( - App.strings.cdwPinCaption(), - textAlign = TextAlign.Center, - style = AppTheme.typography.body2l, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun EnterCardAccessNumber( - can: String, - onCanChange: (String) -> Unit, - onEnter: () -> Unit -) { - Box(Modifier.fillMaxSize().padding(PaddingDefaults.Medium)) { - Column(Modifier.fillMaxSize().align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally) { - Image( - painterResource("images/card_wall_card_can.webp"), - null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.width(278.dp) - ) - Spacer(Modifier.size(16.dp)) - - var isFocussed by remember { mutableStateOf(false) } - - CredentialsTextField( - value = can, - valuePattern = """^\d{0,6}$""".toRegex(), - onValueChange = onCanChange, - onEnter = onEnter, - modifier = Modifier - .fillMaxWidth() - .padding(start = PaddingDefaults.Large, bottom = 8.dp, end = PaddingDefaults.Large) - .onFocusChanged { - isFocussed = it.isFocused - } - ) { - Box(modifier = Modifier.fillMaxWidth()) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.align(Alignment.Center) - ) { - val shape = RoundedCornerShape(8.dp) - val backgroundColor = AppTheme.colors.neutral200 - val borderModifier = Modifier.border( - BorderStroke(1.dp, color = AppTheme.colors.primary700), - shape - ) - - repeat(6) { - Box( - modifier = Modifier - .size(40.dp, 48.dp) - .shadow(1.dp, shape) - .then( - if ((can.length == it || it == 5 && can.length == 6) && isFocussed) { - borderModifier - } else { - Modifier - } - ) - .background( - color = backgroundColor, - shape - ) - .graphicsLayer { - clip = false - } - ) { - Text( - text = can.getOrNull(it)?.toString() ?: " ", - style = MaterialTheme.typography.h6, - modifier = Modifier - .align(Alignment.Center) - .clearAndSetSemantics { } - ) - } - } - } - } - } - Spacer(Modifier.height(PaddingDefaults.Medium)) - Text( - App.strings.cdwCanCaption(), - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -private fun ReadHealthCardAndDownloadData( - authState: AuthenticationState, - onReenterPin: () -> Unit, - onReenterCan: () -> Unit, - onRetry: () -> Unit, - onSuccess: () -> Unit -) { - Box(Modifier.fillMaxSize().padding(PaddingDefaults.Medium), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - val icSize = Modifier.size(64.dp) - if (!authState.isFailure()) { - CircularProgressIndicator(color = AppTheme.colors.primary400, modifier = icSize) - } else { - Icon(Icons.Rounded.Error, null, modifier = icSize, tint = AppTheme.colors.red600) - } - SpacerMedium() - - when { - authState.isInProgress() && authState == AuthenticationState.AuthenticationFlowInitialized -> - Text( - "Bitte jetzt die Gesundheitskarte auflegen", - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - - authState.isInProgress() && authState != AuthenticationState.AuthenticationFlowInitialized -> - Text( - "Gesundheitskarte wird gelesen und Rezepte werden geladen", - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } - - SpacerMedium() - - if (authState.isFailure()) { - val title = when (authState) { - AuthenticationState.HealthCardCardAccessNumberWrong -> - App.strings.cdwNfcIntroStep2HeaderOnCanError() - - AuthenticationState.HealthCardPin2RetriesLeft -> - App.strings.cdwNfcIntroStep2HeaderOnPinError(count = 2, 2) - - AuthenticationState.HealthCardPin1RetryLeft -> - App.strings.cdwNfcIntroStep2HeaderOnPinError(count = 1, 1) - - AuthenticationState.HealthCardBlocked -> - App.strings.cdwNfcIntroStep2HeaderOnCardBlocked() - - AuthenticationState.IDPCommunicationFailed -> - App.strings.cdwNfcIntroStep1HeaderOnError() - - AuthenticationState.HealthCardCommunicationInterrupted -> - App.strings.desktopLoginPageConnectHealthcardErrorIoTitle() - - else -> - App.strings.cdwNfcIntroStep1HeaderOnError() - } - val subtitle = when (authState) { - AuthenticationState.HealthCardCardAccessNumberWrong -> - App.strings.cdwNfcIntroStep2InfoOnCanError() - - AuthenticationState.HealthCardPin2RetriesLeft -> - App.strings.cdwNfcIntroStep2InfoOnPinError(count = 2, 2) - - AuthenticationState.HealthCardPin1RetryLeft -> - App.strings.cdwNfcIntroStep2InfoOnPinError(count = 1, 1) - - AuthenticationState.HealthCardBlocked -> - App.strings.cdwNfcIntroStep2InfoOnCardBlocked() - - AuthenticationState.IDPCommunicationFailed -> - App.strings.cdwNfcIntroStep1InfoOnError() - - AuthenticationState.HealthCardCommunicationInterrupted -> - App.strings.desktopLoginPageConnectHealthcardErrorIoSubtitle() - - else -> - App.strings.cdwNfcIntroStep1InfoOnError() - } - val button = when (authState) { - AuthenticationState.HealthCardCardAccessNumberWrong, - AuthenticationState.HealthCardPin2RetriesLeft, - AuthenticationState.HealthCardPin1RetryLeft -> - App.strings.cdwAuthRetryPinCan() - - else -> - App.strings.cdwAuthRetry() - } - Text(title, style = MaterialTheme.typography.subtitle1, textAlign = TextAlign.Center) - SpacerSmall() - Text(subtitle, style = AppTheme.typography.body2l, textAlign = TextAlign.Center) - SpacerMedium() - Button(onClick = { - when (authState) { - AuthenticationState.HealthCardCardAccessNumberWrong -> onReenterCan() - AuthenticationState.HealthCardPin2RetriesLeft, - AuthenticationState.HealthCardPin1RetryLeft -> onReenterPin() - - else -> onRetry() - } - }) { - Text(button) - } - } else { - onSuccess() - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt deleted file mode 100644 index a80020fc..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.login.ui - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.DownloadUseCase -import de.gematik.ti.erp.app.cardwall.AuthenticationState -import de.gematik.ti.erp.app.cardwall.AuthenticationUseCase -import de.gematik.ti.erp.app.nfc.model.card.NfcHealthCard -import de.gematik.ti.erp.app.smartcard.CardFactory -import de.gematik.ti.erp.app.smartcard.CardReader -import io.github.aakira.napier.Napier -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.transform -import kotlinx.coroutines.withContext -import org.kodein.di.bindings.ScopeCloseable -import java.util.concurrent.Executors - -class LoginWithHealthCardViewModel( - private val dispatchProvider: DispatchProvider, - private val authenticationUseCase: AuthenticationUseCase, - private val downloadUseCase: DownloadUseCase -) : ScopeCloseable { - private val executor = Executors.newSingleThreadExecutor() - private val cardReaderDispatcher = executor.asCoroutineDispatcher() - - suspend fun waitForAnyReader() { - withContext(cardReaderDispatcher) { - var reader: CardReader? = null - do { - try { - reader = CardFactory.instance.readers.firstOrNull() - if (reader == null) { - delay(1000) - } - } catch (e: Exception) { - Napier.e("", e) - delay(2000) - } - } while (reader == null) - } - } - - fun authenticate(can: String, pin: String): Flow = - authenticationUseCase.authenticateWithHealthCard( - can = can, - pin = pin, - flow { - var reader: CardReader? - do { - reader = CardFactory.instance.readers.find { it.isCardPresent } - - delay(1000) - } while (reader == null) - - emit(NfcHealthCard.connect(reader)) - }.flowOn(cardReaderDispatcher) - ) - .flowOn(cardReaderDispatcher) - .transform { state -> - if (state.isFinal()) { - downloadUseCase.update() - .onFailure { - Napier.e("Downloading resources failed", it) - emit(AuthenticationState.IDPCommunicationFailed) - } - .onSuccess { - emit(state) - } - } else { - emit(state) - } - } - .flowOn(dispatchProvider.io) - - override fun close() { - executor.shutdownNow() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/model/LoginWithHealthCard.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/model/LoginWithHealthCard.kt deleted file mode 100644 index 6c55619b..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/model/LoginWithHealthCard.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.login.ui.model -// -// object LoginWithHealthCardData { -// data class LoginWithHealthCardViewModelState( -// -// ) -// } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/repository/LocalDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/repository/LocalDataSource.kt deleted file mode 100644 index 2b72a527..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/repository/LocalDataSource.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.main.repository - -class LocalDataSource diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/LoggedInScreenScaffold.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/LoggedInScreenScaffold.kt deleted file mode 100644 index e719a3af..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/LoggedInScreenScaffold.kt +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.main.ui - -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.selection.toggleable -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.LocalElevationOverlay -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Archive -import androidx.compose.material.icons.outlined.ChatBubbleOutline -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.Logout -import androidx.compose.material.icons.outlined.Medication -import androidx.compose.material.icons.outlined.Message -import androidx.compose.material.icons.outlined.ZoomIn -import androidx.compose.material.icons.outlined.ZoomOut -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.HorizontalDivider -import de.gematik.ti.erp.app.common.SpacerMedium -import de.gematik.ti.erp.app.common.SpacerSmall -import de.gematik.ti.erp.app.common.VerticalDivider -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.communication.ui.CommunicationScreen -import de.gematik.ti.erp.app.navigation.ui.Navigation -import de.gematik.ti.erp.app.prescription.ui.PrescriptionScreen -import de.gematik.ti.erp.app.protocol.ui.ProtocolScreen -import java.util.Locale -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch - -@Composable -fun LoggedInScreen( - mainViewModel: MainScreenViewModel, - navigation: Navigation -) { - Scaffold( - backgroundColor = MaterialTheme.colors.surface - ) { - Row(Modifier.fillMaxSize()) { - SideBar( - mainViewModel = mainViewModel, - navigation = navigation - ) - VerticalDivider() - Column { - val title = when (navigation.currentBackStackEntry) { - MainNavigation.PrescriptionsUnredeemed -> App.strings.desktopMainRedeemablePrescriptions() - MainNavigation.PrescriptionsRedeemed -> App.strings.desktopMainRedeemedPrescriptions() - MainNavigation.PharmacyCommunications -> App.strings.desktopMainRedeemedPrescriptions() - else -> "" - } - TopBar(title) - HorizontalDivider() - Box(Modifier.fillMaxSize()) { - when (navigation.currentBackStackEntry) { - is MainNavigation.Prescriptions -> PrescriptionScreen(navigation) - is MainNavigation.PharmacyCommunications -> CommunicationScreen() - is MainNavigation.Protocol -> ProtocolScreen() - } - } - } - } - } -} - -@Composable -private fun DarkModeToggle( - toggled: Boolean, - onToggle: (Boolean) -> Unit -) { - val handleOffset by animateDpAsState(if (toggled) 32.dp - 20.dp else 0.dp) - Box( - Modifier.toggleable( - toggled, - onValueChange = onToggle, - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) - ) { - Box( - Modifier - .background(AppTheme.colors.neutral400, CircleShape) - .requiredSize(width = 32.dp, height = 14.dp) - .align(Alignment.Center) - ) - DarkModeHandle( - modifier = Modifier.offset(x = handleOffset), - backgroundColor = AppTheme.colors.neutral100 - ) { - Crossfade(toggled) { - when (it) { - true -> Image(painterResource("images/nights_stay.svg"), null) - false -> Image(painterResource("images/wb_sunny.svg"), null) - } - } - } - } -} - -@Composable -private fun DarkModeHandle( - modifier: Modifier, - backgroundColor: Color, - elevation: Dp = 2.dp, - icon: @Composable () -> Unit -) { - val shape = CircleShape - Box( - modifier - .shadow(elevation, shape) - .requiredSize(20.dp) - .background(LocalElevationOverlay.current?.apply(backgroundColor, elevation) ?: backgroundColor, shape) - .clip(shape), - contentAlignment = Alignment.Center - ) { - Box(Modifier.requiredSize(12.dp)) { - icon() - } - } -} - -@Composable -private fun TopBar( - title: String -) { - Row( - Modifier.requiredHeight(56.dp).padding(horizontal = PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - Text(title, style = MaterialTheme.typography.h6) - Spacer(Modifier.weight(1f)) - Logo( - Modifier.height(24.dp) - ) - } -} - -@Composable -private fun SideBar( - mainViewModel: MainScreenViewModel, - navigation: Navigation -) { - val mainState by produceState(mainViewModel.defaultState) { - mainViewModel.screenState().collect { - value = it - } - } - - val selected = navigation.currentBackStackEntry - - val coScope = rememberCoroutineScope() - Column(Modifier.width(224.dp)) { - Row( - Modifier.padding(horizontal = PaddingDefaults.Medium).height(56.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painterResource("images/erp_logo.webp"), - null, - modifier = Modifier.size(32.dp) - ) - Spacer(Modifier.weight(1f)) - DarkModeToggle( - toggled = mainState.darkMode, - onToggle = { if (it) mainViewModel.onEnableDarkMode() else mainViewModel.onDisableDarkMode() } - ) - } - SpacerMedium() - Text( - App.strings.desktopMainPrescriptions().uppercase(Locale.getDefault()), - style = AppTheme.typography.overlinel, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - Column( - Modifier.padding(PaddingDefaults.Small), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - SideBarItem( - Icons.Outlined.Medication, - App.strings.desktopMainRedeemablePrescriptions(), - style = AppTheme.typography.subtitle2l, - selected = selected == MainNavigation.PrescriptionsUnredeemed - ) { - navigation.navigate(MainNavigation.PrescriptionsUnredeemed) - } - SideBarItem( - Icons.Outlined.Archive, - App.strings.desktopMainRedeemedPrescriptions(), - style = AppTheme.typography.subtitle2l, - selected = selected == MainNavigation.PrescriptionsRedeemed - ) { - navigation.navigate(MainNavigation.PrescriptionsRedeemed) - } - SideBarItem( - Icons.Outlined.History, - App.strings.desktopMainPharmacyProtocol(), - style = AppTheme.typography.subtitle2l, - selected = selected == MainNavigation.Protocol - ) { - navigation.navigate(MainNavigation.Protocol) - } - } - SpacerMedium() - Text( - App.strings.desktopMainCommunications().uppercase(Locale.getDefault()), - style = AppTheme.typography.overlinel, - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) - ) - Column( - Modifier.padding(PaddingDefaults.Small), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - SideBarItem( - Icons.Outlined.Message, - App.strings.desktopMainPharmacyCommunications(), - style = AppTheme.typography.subtitle2l, - selected = selected == MainNavigation.PharmacyCommunications - ) { - navigation.navigate(MainNavigation.PharmacyCommunications) - } - } - Spacer(Modifier.weight(1f)) - Column( - Modifier.padding(PaddingDefaults.Small), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - val uriHandler = LocalUriHandler.current - val helpLink = App.strings.desktopHelpLink() - if (mainState.zoomed) { - SideBarItem(Icons.Outlined.ZoomOut, App.strings.desktopMainZoomOut()) { - mainViewModel.onZoomOut() - } - } else { - SideBarItem(Icons.Outlined.ZoomIn, App.strings.desktopMainZoomIn()) { - mainViewModel.onZoomIn() - } - } - SideBarItem(Icons.Outlined.ChatBubbleOutline, App.strings.desktopMainHelp()) { - uriHandler.openUri(helpLink) - } - SideBarItem(Icons.Outlined.Logout, App.strings.desktopMainLogout()) { - coScope.launch { mainViewModel.onLogout() } - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun SideBarItem( - icon: ImageVector, - text: String, - selected: Boolean = false, - style: TextStyle = AppTheme.typography.body2l, - onClick: () -> Unit -) { - Surface( - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - color = if (selected) AppTheme.colors.neutral100 else Color.Unspecified - ) { - Row( - Modifier.padding(horizontal = PaddingDefaults.Small, vertical = PaddingDefaults.Small), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(icon, null, tint = AppTheme.colors.neutral500) - SpacerSmall() - Text(text, style = style) - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreen.kt deleted file mode 100644 index b179a06e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreen.kt +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.main.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asComposeImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import com.google.zxing.common.BitMatrix -import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.ClosablePopupScaffold -import de.gematik.ti.erp.app.common.OverlayPopup -import de.gematik.ti.erp.app.common.SpacerLarge -import de.gematik.ti.erp.app.common.SpacerMedium -import de.gematik.ti.erp.app.common.SpacerSmall -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.login.ui.LoginWithHealthCard -import de.gematik.ti.erp.app.login.ui.LoginWithHealthCardViewModel -import de.gematik.ti.erp.app.navigation.ui.Destination -import de.gematik.ti.erp.app.navigation.ui.Navigation -import de.gematik.ti.erp.app.prescription.usecase.createMatrixCode -import de.gematik.ti.erp.app.rememberScope -import org.jetbrains.skia.Bitmap -import org.jetbrains.skia.ColorAlphaType -import org.jetbrains.skia.ImageInfo -import org.kodein.di.bind -import org.kodein.di.compose.rememberInstance -import org.kodein.di.compose.subDI -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import kotlin.math.roundToInt - -object MainNavigation { - // marker interfaces providing some context - interface Onboarding : Destination - interface Popup : Destination - interface Prescriptions : Destination - interface Communications : Destination - - // onboarding - object Welcome : Onboarding - object Login : Onboarding, Popup - - // prescriptions - object PrescriptionsRedeemed : Prescriptions - object PrescriptionsUnredeemed : Prescriptions - data class DataMatrixCode(val taskId: String, val accessCode: String) : Prescriptions, Popup - - // communications - object PharmacyCommunications : Communications - - object Protocol : Destination -} - -@Composable -fun MainScreen( - mainViewModel: MainScreenViewModel, - navigation: Navigation -) { - val showPopup = navigation.currentBackStackEntry is MainNavigation.Popup - - OverlayPopup( - showPopup, - popupContent = { - val destination = remember { navigation.currentBackStackEntry } - when (destination) { - is MainNavigation.Login -> LoginWithHealthCard(navigation) - is MainNavigation.DataMatrixCode -> DataMatrixCode( - navigation, - taskId = destination.taskId, - accessCode = destination.accessCode - ) - } - } - ) { - when (navigation.currentBackStackEntry) { - is MainNavigation.Onboarding -> - InitialWelcomeScreen(onClickShowLogin = { navigation.navigate(MainNavigation.Login) }) - is MainNavigation.Protocol, - is MainNavigation.Communications, - is MainNavigation.Prescriptions -> - LoggedInScreen( - mainViewModel = mainViewModel, - navigation = navigation - ) - } - } -} - -@Composable -private fun DataMatrixCode( - navigation: Navigation, - taskId: String, - accessCode: String -) { - ClosablePopupScaffold(onClose = { navigation.back() }) { - Box(Modifier.fillMaxSize()) { - Surface( - color = Color.White, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .padding(PaddingDefaults.Medium) - .align(Alignment.Center) - - ) { - DataMatrixCode( - taskId = taskId, - accessCode = accessCode, - modifier = Modifier.aspectRatio(1f) - ) - } - } - } -} - -@Composable -private fun DataMatrixCode( - taskId: String, - accessCode: String, - modifier: Modifier -) { - val matrix = remember { createMatrixCode(taskId, accessCode) } - Box( - modifier = Modifier - .then(modifier) - .background(Color.White) - .padding(PaddingDefaults.Medium) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .drawWithCache { - onDrawBehind { - drawImage( - matrix.toImageBitmap(), - dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()), - filterQuality = FilterQuality.None - ) - } - } - ) - } -} - -private fun BitMatrix.toImageBitmap(): ImageBitmap { - val bytesPerPixel = 4 - val pixels = ByteArray(width * height * bytesPerPixel) - - var i = 0 - for (y in 0 until height) { - for (x in 0 until width) { - val rgb = (if (get(x, y)) 0x00 else 0xff).toByte() - pixels[i++] = rgb // b - pixels[i++] = rgb // g - pixels[i++] = rgb // r - pixels[i++] = 0xff.toByte() // a - } - } - - val bitmap = Bitmap() - bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.UNPREMUL)) - bitmap.installPixels(pixels) - return bitmap.asComposeImageBitmap() -} - -@Composable -private fun LoginWithHealthCard( - navigation: Navigation -) { - val scope = rememberScope() - - subDI(diBuilder = { - bind { - scoped(scope).singleton { - LoginWithHealthCardViewModel(instance(), instance(), instance()) - } - } - }) { - val loginWithHealthCardViewModel by rememberInstance() - - LoginWithHealthCard( - loginWithHealthCardViewModel, - onFinished = { navigation.navigate(MainNavigation.PrescriptionsUnredeemed) }, - onClose = { navigation.back() } - ) - } -} - -@Composable -fun Logo( - modifier: Modifier = Modifier -) { - Row( - modifier, - verticalAlignment = Alignment.CenterVertically - ) { - val colorFilter = if (MaterialTheme.colors.isLight) null else ColorFilter.tint(AppTheme.colors.neutral800) - Image( - painterResource("images/ic_onboarding_logo_flag.xml"), - null, - modifier = Modifier.height(16.dp).wrapContentWidth(), - contentScale = ContentScale.FillHeight, - colorFilter = colorFilter - ) - SpacerSmall() - Image( - painterResource("images/ic_onboarding_logo_gematik.xml"), - null, - modifier = Modifier.height(32.dp).wrapContentWidth(), - contentScale = ContentScale.FillHeight, - colorFilter = colorFilter - ) - } -} - -@Composable -fun InitialWelcomeScreen( - onClickShowLogin: () -> Unit -) { - Scaffold( - backgroundColor = MaterialTheme.colors.surface - ) { - Column(Modifier.fillMaxSize()) { - Logo( - Modifier.padding(top = 48.dp, start = 96.dp).height(32.dp) - ) - Spacer(Modifier.weight(1f)) - Column(Modifier.align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - if (BuildKonfig.INTERNAL) { - Image( - painterResource("images/erp_logo_dev.webp"), - null, - modifier = Modifier.size(100.dp) - ) - SpacerMedium() - } - Text(App.strings.desktopMainWelcomeTitle(), style = MaterialTheme.typography.h5) - SpacerSmall() - Text(App.strings.desktopMainWelcomeSubtitle(), style = MaterialTheme.typography.body1) - SpacerLarge() - Button(onClick = onClickShowLogin) { - Text(App.strings.desktopMainLoginWithHealthcard()) - } - } - Spacer(Modifier.weight(1f)) - Image( - painterResource("images/crew.webp"), - null, - alignment = Alignment.BottomCenter, - modifier = Modifier - .fillMaxWidth() - .height(200.dp) - ) - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreenViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreenViewModel.kt deleted file mode 100644 index ae0551da..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/MainScreenViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.main.ui - -import de.gematik.ti.erp.app.main.ui.model.MainScreenData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlin.system.exitProcess - -class MainScreenViewModel( - private val zoomRange: ClosedFloatingPointRange, - private val defaultZoom: Float, - private val defaultDarkMode: Boolean -) { - val defaultState = MainScreenData.State(zoomed = false, zoom = defaultZoom, darkMode = defaultDarkMode) - private val state = MutableStateFlow(defaultState) - - fun screenState(): Flow = state - - fun onZoom(step: Float) { - state.value = state.value.copy(zoomed = true, zoom = (state.value.zoom + step).coerceIn(zoomRange)) - } - - fun onZoomIn() { - state.value = state.value.copy(zoomed = true, zoom = zoomRange.endInclusive) - } - - fun onZoomOut() { - state.value = state.value.copy(zoomed = false, zoom = defaultZoom) - } - - fun onEnableDarkMode() { - state.value = state.value.copy(darkMode = true) - } - - fun onDisableDarkMode() { - state.value = state.value.copy(darkMode = false) - } - - suspend fun onLogout() { - exitProcess(0) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/model/MainScreenData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/model/MainScreenData.kt deleted file mode 100644 index 736d98f1..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/main/ui/model/MainScreenData.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.main.ui.model - -object MainScreenData { - data class State( - val zoomed: Boolean, - val zoom: Float, - val darkMode: Boolean - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/navigation/ui/Navigation.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/navigation/ui/Navigation.kt deleted file mode 100644 index 3ad116a0..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/navigation/ui/Navigation.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.navigation.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember - -@Stable -class Navigation(startDestination: Destination) { - private val observableBackStack = mutableStateListOf(startDestination) - - val backStackEntries: List - get() = observableBackStack - - val currentBackStackEntry: Destination? - get() = observableBackStack.lastOrNull() - - fun back() { - if (observableBackStack.isNotEmpty()) { - observableBackStack.removeLast() - } - } - - fun navigate(destination: Destination) { - observableBackStack.add(destination) - } - - fun navigate(destination: Destination, clearBackStack: Boolean) { - if (clearBackStack) { - observableBackStack.clear() - } - observableBackStack.add(destination) - } -} - -interface Destination - -@Composable -fun rememberNavigation(startDestination: Destination): Navigation { - return remember { Navigation(startDestination) } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/di/NetworkModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/di/NetworkModule.kt deleted file mode 100644 index 8e280361..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/di/NetworkModule.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.network.di - -import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.parser.IParser -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import de.gematik.ti.erp.app.Constants -import de.gematik.ti.erp.app.JWSConverterFactory -import de.gematik.ti.erp.app.api.ErpService -import de.gematik.ti.erp.app.applicationScope -import de.gematik.ti.erp.app.common.pinning.buildCertificatePinner -import de.gematik.ti.erp.app.core.FhirConverterFactory -import de.gematik.ti.erp.app.core.NapierLogger -import de.gematik.ti.erp.app.core.PrefixedLogger -import de.gematik.ti.erp.app.idp.api.IdpService -import de.gematik.ti.erp.app.network.interceptor.ApiKeyHeaderInterceptor -import de.gematik.ti.erp.app.network.interceptor.BearerHeaderInterceptor -import de.gematik.ti.erp.app.network.interceptor.UserAgentHeaderInterceptor -import de.gematik.ti.erp.app.vau.api.VauService -import de.gematik.ti.erp.app.vau.interceptor.VauChannelInterceptor -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import okhttp3.CipherSuite -import okhttp3.ConnectionSpec -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.TlsVersion -import okhttp3.logging.HttpLoggingInterceptor -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindInstance -import org.kodein.di.bindMultiton -import org.kodein.di.bindProvider -import org.kodein.di.bindSingleton -import org.kodein.di.bindings.ScopeCloseable -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import retrofit2.Converter -import retrofit2.Retrofit - -@JvmInline -value class ScopedOkHttpClient(val client: OkHttpClient) : ScopeCloseable { - override fun close() { - client.dispatcher.executorService.shutdown() - client.connectionPool.evictAll() - } -} - -@OptIn(ExperimentalSerializationApi::class) -val networkModule = DI.Module("Network Module") { - bindInstance { Json { ignoreUnknownKeys = true } } - bindSingleton("jsonConverterFactory") { instance().asConverterFactory("application/json".toMediaType()) } - bind { - scoped(applicationScope).singleton { - ScopedOkHttpClient( - OkHttpClient.Builder() - .certificatePinner(buildCertificatePinner()) - .connectionSpecs( - listOf( - ConnectionSpec - .Builder(ConnectionSpec.RESTRICTED_TLS) - .tlsVersions( - TlsVersion.TLS_1_2, - TlsVersion.TLS_1_3 - ) - .cipherSuites( - // TLS 1.2 - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - // TLS 1.3 - CipherSuite.TLS_AES_128_GCM_SHA256, - CipherSuite.TLS_AES_256_GCM_SHA384, - CipherSuite.TLS_CHACHA20_POLY1305_SHA256 - ) - .build() - ) - ) - .build() - ) - } - } - bindSingleton { FhirContext.forR4().newJsonParser() } - bindSingleton { UserAgentHeaderInterceptor() } - bindSingleton { ApiKeyHeaderInterceptor() } - bindSingleton { BearerHeaderInterceptor(instance()) } - - bindProvider { - HttpLoggingInterceptor(NapierLogger()).also { - it.setLevel(HttpLoggingInterceptor.Level.BODY) - } - } - - bindMultiton("prefixed") { prefix -> - HttpLoggingInterceptor(PrefixedLogger(prefix)).also { - it.setLevel(HttpLoggingInterceptor.Level.BODY) - } - } - - bindSingleton { - val clientBuilder = instance().client.newBuilder() - val userAgentInterceptor = instance() - val apiKeyInterceptor = instance() - val loggingInterceptor = instance() - - clientBuilder - .addInterceptor(userAgentInterceptor) - .addInterceptor(apiKeyInterceptor) - .addInterceptor(loggingInterceptor) - .followRedirects(false) - - Retrofit.Builder() - .client(clientBuilder.build()) - .baseUrl(Constants.IDP.serviceUri) - .addConverterFactory(JWSConverterFactory()) - .addConverterFactory(instance("jsonConverterFactory")) - .build() - .create(IdpService::class.java) - } - - bindSingleton { - val clientBuilder = instance().client.newBuilder() - val fhirParser = instance() - val vauChannelInterceptor = instance() - val userAgentInterceptor = instance() - val apiKeyInterceptor = instance() - val bearerInterceptor = instance() - val innerLoggingInterceptor = instance("prefixed", "inner request") - val outerLoggingInterceptor = instance("prefixed", "inner request") - - clientBuilder.addInterceptor(bearerInterceptor) - - clientBuilder.addInterceptor(innerLoggingInterceptor) - - clientBuilder.addInterceptor(vauChannelInterceptor) - - // user agent & dev headers at outer request - clientBuilder.addInterceptor(userAgentInterceptor) - clientBuilder.addInterceptor(apiKeyInterceptor) - - clientBuilder.addInterceptor(outerLoggingInterceptor) - - Retrofit.Builder() - .client(clientBuilder.build()) - .baseUrl(Constants.ERP.serviceUri) - .addConverterFactory(FhirConverterFactory.create(fhirParser)) - .addConverterFactory(instance("jsonConverterFactory")) - .build() - .create(ErpService::class.java) - } - - // The VAU service is only used to get CertList & OCSPList and NOT to post to the VAU endpoint - bindSingleton { - val clientBuilder = instance().client.newBuilder() - val userAgentInterceptor = instance() - val apiKeyInterceptor = instance() - val loggingInterceptor = instance() - - clientBuilder.addInterceptor(apiKeyInterceptor) - clientBuilder.addInterceptor(userAgentInterceptor) - clientBuilder.addInterceptor(loggingInterceptor) - - Retrofit.Builder() - .client(clientBuilder.build()) - .baseUrl(Constants.ERP.serviceUri) - .addConverterFactory(instance("jsonConverterFactory")) - .build() - .create(VauService::class.java) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/BearerHeaderInterceptor.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/BearerHeaderInterceptor.kt deleted file mode 100644 index e56a2455..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/BearerHeaderInterceptor.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.network.interceptor - -import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import io.github.aakira.napier.Napier -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import java.net.HttpURLConnection - -class BearerHeaderInterceptor( - private val idpUseCase: IdpUseCase -) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val original: Request = chain.request() - - val response = chain.proceed(request(original, loadAccessToken(false))) - return if (response.code == HttpURLConnection.HTTP_UNAUTHORIZED) { - Napier.d("Received 401 -> refresh access token") - - chain.proceed(request(original, loadAccessToken(true))) - } else { - response - } - } - - private fun loadAccessToken(refresh: Boolean) = - runBlocking { idpUseCase.loadAccessToken(refresh) } - - private fun request(original: Request, token: String) = - original.newBuilder() - .header("Accept", "application/fhir+json") - .header("Content-Type", "application/fhir+json; charset=UTF-8") - .header( - "Authorization", - "Bearer $token" - ) - .build() -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/HeaderInterceptor.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/HeaderInterceptor.kt deleted file mode 100644 index da76d12e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/network/interceptor/HeaderInterceptor.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.network.interceptor - -import de.gematik.ti.erp.app.Constants -import okhttp3.Interceptor -import okhttp3.Response - -class UserAgentHeaderInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request().newBuilder() - .header("User-Agent", Constants.userAgent) - .build() - - return chain.proceed(request) - } -} - -class ApiKeyHeaderInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request().newBuilder() - .header("X-Api-Key", Constants.ERP.apiKey) - .build() - - return chain.proceed(request) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardChannel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardChannel.kt deleted file mode 100644 index c94a4aaa..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardChannel.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.nfc.model.card - -import de.gematik.ti.erp.app.card.model.card.ICardChannel -import de.gematik.ti.erp.app.card.model.command.CommandApdu -import de.gematik.ti.erp.app.card.model.command.ResponseApdu -import java.io.Closeable - -class NfcCardChannel internal constructor( - override val isExtendedLengthSupported: Boolean, - private val nfcHealthCard: NfcHealthCard -) : ICardChannel, Closeable { - override val card: NfcHealthCard - get() = nfcHealthCard - - override val maxTransceiveLength = 1024 - - /** - * Returns the responseApdu after transmitting a commandApdu - */ - override fun transmit(command: CommandApdu): ResponseApdu = - nfcHealthCard.transmit(command) - - override fun close() { - card.card.close() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardSecureChannel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardSecureChannel.kt deleted file mode 100644 index 9ba8aaca..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcCardSecureChannel.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.nfc.model.card - -import de.gematik.ti.erp.app.card.model.card.ICardChannel -import de.gematik.ti.erp.app.card.model.card.IHealthCard -import de.gematik.ti.erp.app.card.model.card.PaceKey -import de.gematik.ti.erp.app.card.model.card.SecureMessaging -import de.gematik.ti.erp.app.card.model.command.CommandApdu -import de.gematik.ti.erp.app.card.model.command.ResponseApdu -import io.github.aakira.napier.Napier - -class NfcCardSecureChannel internal constructor( - override val isExtendedLengthSupported: Boolean, - private val nfcHealthCard: NfcHealthCard, - paceKey: PaceKey -) : ICardChannel { - private var secureMessaging = SecureMessaging(paceKey) - - override val card: IHealthCard - get() = nfcHealthCard - - override val maxTransceiveLength = 1024 - - /** - * Returns the responseApdu after transmitting a commandApdu - */ - override fun transmit(command: CommandApdu): ResponseApdu { - Napier.d("Encrypt ----") - return secureMessaging.encrypt(command).let { encryptedCommand -> - Napier.d("encrypted ----") - nfcHealthCard.transmit(encryptedCommand).let { encryptedResponse -> - Napier.d("Decrypt ----") - secureMessaging.decrypt(encryptedResponse) - } - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcHealthCard.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcHealthCard.kt deleted file mode 100644 index 889a6d66..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/nfc/model/card/NfcHealthCard.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.nfc.model.card - -import de.gematik.ti.erp.app.card.model.card.IHealthCard -import de.gematik.ti.erp.app.card.model.command.CommandApdu -import de.gematik.ti.erp.app.card.model.command.ResponseApdu -import de.gematik.ti.erp.app.smartcard.Card -import de.gematik.ti.erp.app.smartcard.CardReader -import java.nio.ByteBuffer - -class NfcHealthCard private constructor(val card: Card) : IHealthCard { - private val buffer = ByteBuffer.allocate(1024) - - override fun transmit(apduCommand: CommandApdu): ResponseApdu { - buffer.clear() - val n = card.transmit(ByteBuffer.wrap(apduCommand.bytes), buffer) - return ResponseApdu(buffer.array().copyOfRange(0, n)) - } - - companion object { - fun connect(reader: CardReader): NfcCardChannel = - NfcCardChannel(isExtendedLengthSupported = true, NfcHealthCard(reader.connect())) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt deleted file mode 100644 index b5cd6d81..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.di - -import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource -import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionMapper -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindInstance -import org.kodein.di.bindings.Scope -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton - -fun prescriptionModule(scope: Scope) = DI.Module("Prescription Module") { - bind { scoped(scope).singleton { PrescriptionRemoteDataSource(instance()) } } - bind { scoped(scope).singleton { PrescriptionLocalDataSource() } } - bind { scoped(scope).singleton { DesktopPrescriptionRepository(instance(), instance(), instance(), instance()) } } - bind { scoped(scope).singleton { PrescriptionUseCase(instance(), instance()) } } - bindInstance { PrescriptionMapper() } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt deleted file mode 100644 index 846d720f..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.fhir.FhirMapper -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.withContext - -class DesktopPrescriptionRepository( - private val dispatchProvider: DispatchProvider, - private val localDataSource: PrescriptionLocalDataSource, - private val remoteDataSource: PrescriptionRemoteDataSource, - private val mapper: FhirMapper -) { - fun tasks() = localDataSource.loadTasks() - fun auditEvents() = localDataSource.loadAuditEvents() - fun medicationDispenses() = localDataSource.loadMedicationDispenses() - - suspend fun download(): Result = - loadTasksRemote().mapCatching { taskIds -> - supervisorScope { - withContext(dispatchProvider.io) { - taskIds.map { taskId -> - async { downloadTaskWithKBVBundle(taskId) } - } + async { - downloadAuditEvents() - } + async { - downloadMedicationDispenses() - } - } - .awaitAll() - .find { it.isFailure } - ?.getOrThrow() - } - } - - suspend fun delete(taskId: String) = - remoteDataSource.deleteTask(taskId).mapCatching { - localDataSource.deleteTask(taskId) - } - - suspend fun loadTasksRemote(): Result> = - remoteDataSource.getTasks().mapCatching { bundle -> - mapper.parseTaskIds(bundle) - } - - suspend fun downloadTaskWithKBVBundle(taskId: String): Result = - remoteDataSource.getTaskWithKBVBundle(taskId).mapCatching { - val task = mapper.mapFhirBundleToTaskWithKBVBundle(it) - localDataSource.saveTask(task) - } - - suspend fun downloadAuditEvents(): Result = - remoteDataSource.getAllAuditEvents().mapCatching { - val auditEvents = mapper.mapFhirBundleToAuditEvents(it) - localDataSource.saveAuditEvents(auditEvents) - } - - suspend fun downloadMedicationDispenses(): Result = - remoteDataSource.getAllMedicationDispenses().mapCatching { - val medicationDispenses = mapper.mapFhirMedicationDispenseToSimpleMedicationDispense(it) - localDataSource.saveMedicationDispenses(medicationDispenses) - } - suspend fun invalidate() = localDataSource.invalidate() -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt deleted file mode 100644 index add16e78..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository - -import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent -import de.gematik.ti.erp.app.prescription.repository.model.SimpleMedicationDispense -import de.gematik.ti.erp.app.prescription.repository.model.SimpleTask -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -class LocalDataSource { - private val auditEvents = MutableStateFlow(emptyList()) - private val medicationDispenses = MutableStateFlow(emptyList()) - private val tasks = MutableStateFlow(emptyList()) - private val communications = MutableStateFlow(emptyList()) - private val lock = Mutex() - - suspend fun saveTask(task: SimpleTask) = lock.withLock { - tasks.value = tasks.value.filter { it.taskId != task.taskId } + task - } - - suspend fun deleteTask(taskId: String) = lock.withLock { - tasks.value = tasks.value.filter { it.taskId != taskId } - } - - suspend fun saveAuditEvents(auditEvents: List) = lock.withLock { - val ids = auditEvents.map { it.id } - this.auditEvents.value = this.auditEvents.value.filter { it.id !in ids } + auditEvents - } - - suspend fun saveMedicationDispenses(medicationDispenses: List) = lock.withLock { - val ids = medicationDispenses.map { it.id } - this.medicationDispenses.value = this.medicationDispenses.value.filter { it.id !in ids } + medicationDispenses - } - - suspend fun saveCommunications(communications: List) = lock.withLock { - val ids = communications.map { it.id } - this.communications.value = this.communications.value.filter { it.id !in ids } + communications - } - - fun loadAuditEvents(): Flow> { - return auditEvents - } - - fun loadTasks(): Flow> { - return tasks - } - - fun loadMedicationDispenses(): Flow> { - return medicationDispenses - } - - fun loadCommunications(): Flow> { - return communications - } - - suspend fun invalidate() = lock.withLock { - auditEvents.value = emptyList() - medicationDispenses.value = emptyList() - tasks.value = emptyList() - communications.value = emptyList() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt deleted file mode 100644 index 65bc0095..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository - -import de.gematik.ti.erp.app.api.ErpService -import de.gematik.ti.erp.app.core.safeApiCall -import de.gematik.ti.erp.app.core.safeApiCallNullable -import org.hl7.fhir.r4.model.Bundle - -class RemoteDataSource( - private val service: ErpService -) { - suspend fun getTasks(): Result = - safeApiCall("Error while loading tasks") { service.getAllTasks() } - - suspend fun getTaskWithKBVBundle(taskId: String) = - safeApiCall("Error while downloading KBV Bundle $taskId") { service.getTaskWithKBVBundle(taskId) } - - suspend fun getAllAuditEvents() = - safeApiCall("Error getting all Audit Events") { service.getAllAuditEvents() } - - suspend fun getAllMedicationDispenses() = - safeApiCall("Error getting all Medication Dispenses") { service.getAllMedicationDispenses() } - - suspend fun getAllCommunications(): Result = - safeApiCall("Error getting communications") { service.getAllCommunications() } - - suspend fun deleteTask(taskId: String) = - safeApiCallNullable("Error deleting Task $taskId") { service.deleteTask(taskId) } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleAuditEvent.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleAuditEvent.kt deleted file mode 100644 index 69c27765..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleAuditEvent.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository.model - -import java.time.LocalDateTime - -data class SimpleAuditEvent( - val id: String, - val locale: String, - val text: String, - val timestamp: LocalDateTime, - val taskId: String -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleMedicationDispense.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleMedicationDispense.kt deleted file mode 100644 index 4a7e23b0..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleMedicationDispense.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository.model - -import de.gematik.ti.erp.app.fhir.MedicationDetail -import java.time.LocalDateTime - -data class SimpleMedicationDispense( - val id: String, - val taskId: String, - val patientIdentifier: String, // KVNR - val wasSubstituted: Boolean, - val dosageInstruction: String?, - val performer: String, // Telematik-ID - val whenHandedOver: LocalDateTime, - val medicationDetail: MedicationDetail -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleTask.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleTask.kt deleted file mode 100644 index 12c683c7..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/model/SimpleTask.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.repository.model - -import org.hl7.fhir.r4.model.Bundle as FhirBundle -import java.time.LocalDate -import java.time.LocalDateTime - -data class SimpleTask( - val taskId: String, - val lastModified: LocalDateTime? = null, - - val organization: String, // an organization can contain multiple authors - val medicationText: String?, - val expiresOn: LocalDate, - val acceptUntil: LocalDate, - val authoredOn: LocalDateTime, - - // synced only - val status: String? = null, - - val rawKBVBundle: FhirBundle -) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt deleted file mode 100644 index ac6b8937..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.Dialog -import de.gematik.ti.erp.app.common.HintCard -import de.gematik.ti.erp.app.common.HintCardDefaults -import de.gematik.ti.erp.app.common.HintLargeImage -import de.gematik.ti.erp.app.common.HintSmallImage -import de.gematik.ti.erp.app.common.HintTextLearnMoreButton -import de.gematik.ti.erp.app.common.SpacerTiny -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.fhir.InsuranceCompanyDetail -import de.gematik.ti.erp.app.fhir.MedicationRequestDetail -import de.gematik.ti.erp.app.fhir.OrganizationDetail -import de.gematik.ti.erp.app.fhir.PatientDetail -import de.gematik.ti.erp.app.fhir.PractitionerDetail -import de.gematik.ti.erp.app.fhir.codeToDosageFormMapping -import de.gematik.ti.erp.app.fhir.normSizeMapping -import de.gematik.ti.erp.app.fhir.statusMapping -import de.gematik.ti.erp.app.navigation.ui.Navigation -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.util.Locale - -private const val missingValue = "---" - -@Composable -fun PrescriptionDetailsScreen( - navigation: Navigation, - prescription: PrescriptionUseCaseData.PrescriptionDetails, - audits: List, - onClickDelete: () -> Unit -) { - val lazyListState = rememberLazyListState() - val scrollbarAdapter = rememberScrollbarAdapter(lazyListState) - - // scroll back to the top if we select a new prescription - LaunchedEffect(prescription) { - lazyListState.animateScrollToItem(0) - } - - val dtAuditFormatter = - remember { DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT) } - - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colors.background - ) { - Box { - LazyColumn( - state = lazyListState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - contentPadding = PaddingValues(horizontal = PaddingDefaults.Medium), - modifier = Modifier.widthIn(max = 560.dp).align(Alignment.Center) - ) { - if (prescription.medicationRequest.emergencyFee == true) { - item { - EmergencyServiceCard() - } - } - - item { - Column(Modifier.padding(PaddingDefaults.Medium)) { - SelectionContainer { - Column { - Header( - text = prescription.medication.text ?: missingValue - ) - FullDetailSecondHeader(prescription) - } - } - } - } - - if (prescription.medicationRequest.substitutionAllowed && !prescription.isDispensed) { - item { - SubstitutionAllowed() - } - } - - item { - Column { - MedicationInformation(prescription) - if (prescription.isSubstituted) { - WasSubstitutedHint() - } - DosageInformation(prescription) - PatientInformation(prescription.patient, prescription.insurance) - } - } - - item { - PractitionerInformation(prescription.practitioner) - } - item { - OrganizationInformation(prescription.organization) - } - item { - AccidentInformation(prescription.medicationRequest) - } - item { - SubHeader( - text = App.strings.presDetailProtocolHeader() - ) - } - items(audits) { - Label( - text = if (it.text.isNullOrEmpty()) { - App.strings.presDetailProtocolEmptyText() - } else { - it.text - }, - label = it.timestamp.format(dtAuditFormatter) - ) - } - item { - TechnicalPrescriptionInformation(prescription.prescription) - } - item { - DeleteButton { - onClickDelete() - } - } - } - VerticalScrollbar( - scrollbarAdapter, - modifier = Modifier.align(Alignment.CenterEnd).padding(horizontal = 1.dp).fillMaxHeight() - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun DeleteButton(onClickDelete: () -> Unit) { - var showDeleteDialog by remember { mutableStateOf(false) } - - Button( - onClick = { showDeleteDialog = true }, - modifier = Modifier - .padding( - horizontal = PaddingDefaults.Medium, - vertical = PaddingDefaults.Medium * 2 - ) - .fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.red600, - contentColor = AppTheme.colors.neutral000 - ) - ) { - Text( - App.strings.presDetailDelete().uppercase(Locale.getDefault()), - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - top = PaddingDefaults.Medium / 2, - bottom = PaddingDefaults.Medium / 2 - ) - ) - } - - if (showDeleteDialog) { - Dialog( - title = App.strings.presDetailDeleteMsg(), - confirmButton = { - Button( - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.red600, - contentColor = AppTheme.colors.neutral000 - ), - onClick = { - onClickDelete() - showDeleteDialog = false - } - ) { - Text(App.strings.presDetailDeleteYes().uppercase(Locale.getDefault())) - } - }, - dismissButton = { - TextButton(onClick = { - showDeleteDialog = false - }) { - Text(App.strings.presDetailDeleteNo().uppercase(Locale.getDefault())) - } - }, - onDismissRequest = { showDeleteDialog = false } - ) - } -} - -@Composable -private fun FullDetailSecondHeader( - prescriptionDetails: PrescriptionUseCaseData.PrescriptionDetails -) { - Column { - Text( - text = expiresOrAcceptedUntil(prescriptionDetails.prescription), - style = AppTheme.typography.body2l - ) - SpacerTiny() - } -} - -@Composable -private fun MedicationInformation( - prescription: PrescriptionUseCaseData.PrescriptionDetails -) { - val medicationType = - prescription.medicationType()?.let { App.strings.codeToDosageFormMapping()[it]?.invoke() } ?: missingValue - - val uniqueIdentifier = - prescription.uniqueIdentifier() ?: missingValue - - Column { - SubHeader( - text = App.strings.presDetailMedicationHeader() - ) - - Label( - text = medicationType, - label = App.strings.presDetailMedicationLabelDosageForm() - ) - - Label( - text = if (prescription.medication.normSizeCode != null) { - "${prescription.medication.normSizeCode} - " + - "${App.strings.normSizeMapping()[prescription.medication.normSizeCode]?.invoke()}" - } else { - missingValue - }, - label = App.strings.presDetailMedicationLabelNormsize() - ) - - Label( - text = uniqueIdentifier, - label = App.strings.presDetailMedicationLabelId() - ) - } -} - -@Composable -private fun WasSubstitutedHint() = - HintCard( - modifier = Modifier.padding(PaddingDefaults.Medium), - properties = HintCardDefaults.flatProperties( - backgroundColor = AppTheme.colors.red100, - contentColor = AppTheme.colors.neutral999 - ), - image = { - HintSmallImage( - painterResource("images/medical_hand_out_circle_red.webp"), - innerPadding = it - ) - }, - title = { Text(App.strings.presDetailSubstitutedHeader()) }, - body = { Text(App.strings.presDetailSubstitutedInfo()) } - ) - -@Composable -private fun DosageInformation( - prescription: PrescriptionUseCaseData.PrescriptionDetails -) { - val infoText = prescription.dosageInstruction() ?: App.strings.presDetailDosageDefaultInfo() - - SubHeader( - text = App.strings.presDetailDosageHeader() - ) - HintCard( - modifier = Modifier.padding(start = PaddingDefaults.Medium, end = PaddingDefaults.Medium), - properties = HintCardDefaults.flatProperties( - shape = RoundedCornerShape(8.dp), - backgroundColor = AppTheme.colors.primary100 - ), - image = { HintSmallImage(painterResource("images/doctor_circle.webp"), innerPadding = it) }, - title = null, - body = { Text(infoText) } - ) -} - -@Composable -private fun PatientInformation( - patient: PatientDetail, - insurance: InsuranceCompanyDetail -) { - Column { - val dtFormatter = - remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - - SubHeader( - text = App.strings.presDetailPatientHeader() - ) - - Label( - text = patient.name ?: missingValue, - label = App.strings.presDetailPatientLabelName() - ) - - Label( - text = patient.address ?: missingValue, - label = App.strings.presDetailPatientLabelAddress() - ) - - Label( - text = patient.birthdate?.format(dtFormatter) ?: missingValue, - label = App.strings.presDetailPatientLabelBirthdate() - ) - - Label( - text = insurance.name ?: missingValue, - label = App.strings.presDetailPatientLabelInsurance() - ) - - Label( - text = insurance.statusCode?.let { App.strings.statusMapping()[it]?.invoke() } ?: missingValue, - label = App.strings.presDetailPatientLabelMemberStatus() - ) - - Label( - text = patient.insuranceIdentifier ?: missingValue, - label = App.strings.presDetailPatientLabelInsuranceId() - ) - } -} - -@Composable -private fun PractitionerInformation( - practitioner: PractitionerDetail -) { - Column { - SubHeader( - text = App.strings.presDetailPractitionerHeader() - ) - - Label( - text = practitioner.name ?: missingValue, - label = App.strings.presDetailPractitionerLabelName() - ) - - Label( - text = practitioner.qualification ?: missingValue, - label = App.strings.presDetailPractitionerLabelQualification() - ) - - Label( - text = practitioner.practitionerIdentifier ?: missingValue, - label = App.strings.presDetailPractitionerLabelId() - ) - } -} - -@Composable -private fun OrganizationInformation( - organization: OrganizationDetail -) { - Column { - SubHeader( - text = App.strings.presDetailOrganizationHeader() - ) - - Label( - text = organization.name ?: missingValue, - label = App.strings.presDetailOrganizationLabelName() - ) - - Label( - text = organization.address ?: missingValue, - label = App.strings.presDetailOrganizationLabelAddress() - ) - - Label( - text = organization.uniqueIdentifier ?: missingValue, - label = App.strings.presDetailOrganizationLabelId() - ) - - Label( - text = organization.phone ?: missingValue, - label = App.strings.presDetailOrganizationLabelTelephone() - ) - - Label( - text = organization.mail ?: missingValue, - label = App.strings.presDetailOrganizationLabelEmail() - ) - } -} - -@Composable -private fun AccidentInformation( - medicationRequest: MedicationRequestDetail -) { - val dtFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - - SubHeader( - text = App.strings.presDetailAccidentHeader() - ) - - Label( - text = medicationRequest.dateOfAccident?.format(dtFormatter) ?: missingValue, - label = App.strings.presDetailAccidentLabelDate() - ) - - Label( - text = medicationRequest.location ?: missingValue, - label = App.strings.presDetailAccidentLabelLocation() - ) -} - -@Composable -private fun TechnicalPrescriptionInformation(prescription: PrescriptionUseCaseData.Prescription) { - Column { - SubHeader(App.strings.presDetailTechnicalInformation()) - - Label( - text = prescription.taskId, - label = App.strings.taskId() - ) - } -} - -@Composable -private fun Group( - content: @Composable ColumnScope.() -> Unit -) { - Surface( - Modifier.padding(PaddingDefaults.Medium), - shape = RoundedCornerShape(8.dp), - elevation = 0.dp - ) { - Column { - content() - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun Label( - text: String, - label: String -) { - SelectionContainer { - Column( - modifier = Modifier - .padding(PaddingDefaults.Medium) - .fillMaxWidth() - ) { - Text( - text = text, - style = MaterialTheme.typography.body1 - ) - SpacerTiny() - Text( - text = label, - style = AppTheme.typography.body2l - ) - } - } -} - -@Composable -private fun Header( - text: String, - modifier: Modifier = Modifier -) = - Text( - text = text, - style = MaterialTheme.typography.h5, - fontWeight = FontWeight.Medium, - modifier = modifier.padding( - top = PaddingDefaults.Large - ) - ) - -@Composable -private fun SubHeader( - text: String, - modifier: Modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) -) = - Text( - text = text, - style = MaterialTheme.typography.h6, - fontWeight = FontWeight.Medium, - modifier = modifier.padding( - top = PaddingDefaults.Large + PaddingDefaults.Medium, - bottom = PaddingDefaults.Medium - ) - ) - -@Composable -private fun EmergencyServiceCard( - modifier: Modifier = Modifier -) = - HintCard( - modifier = modifier, - properties = HintCardDefaults.properties(elevation = 0.dp), - image = { - HintLargeImage( - painterResource("images/pharmacist.webp"), - innerPadding = it - ) - }, - title = { Text(App.strings.presDetailNoctuHeader()) }, - body = { Text(App.strings.presDetailNoctuInfo()) } - ) - -@Composable -private fun SubstitutionAllowed() = - HintCard( - modifier = Modifier.padding(PaddingDefaults.Medium), - properties = HintCardDefaults.flatProperties( - backgroundColor = AppTheme.colors.primary100 - ), - image = { - HintSmallImage( - painterResource("images/pharmacist_circle.webp"), - innerPadding = it - ) - }, - title = { Text(App.strings.presDetailAutIdemHeader()) }, - body = { Text(App.strings.presDetailAutIdemInfo()) }, - action = { - HintTextLearnMoreButton() - } - ) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt deleted file mode 100644 index e9b22355..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerMoveFilter -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.common.App -import de.gematik.ti.erp.app.common.Dialog -import de.gematik.ti.erp.app.common.HorizontalDivider -import de.gematik.ti.erp.app.common.HorizontalSplittable -import de.gematik.ti.erp.app.common.SpacerSmall -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.main.ui.MainNavigation -import de.gematik.ti.erp.app.navigation.ui.Navigation -import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import de.gematik.ti.erp.app.rememberScope -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import org.kodein.di.bind -import org.kodein.di.compose.rememberInstance -import org.kodein.di.compose.subDI -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle -import java.util.Locale - -@Composable -fun PrescriptionScreen( - navigation: Navigation -) { - val scope = rememberScope() - - subDI(diBuilder = { - bind { scoped(scope).singleton { PrescriptionViewModel(instance(), instance()) } } - }) { - val prescriptionViewModel by rememberInstance() - val state by produceState(prescriptionViewModel.defaultState) { - prescriptionViewModel.screenState().collect { - value = it - } - } - - DeleteAlertDialog(prescriptionViewModel) - - val coScope = rememberCoroutineScope() - val selectedPrescription = state.selectedPrescription - - LaunchedEffect(navigation.currentBackStackEntry) { - when (navigation.currentBackStackEntry) { - is MainNavigation.PrescriptionsUnredeemed -> - coScope.launch { prescriptionViewModel.onSelectNotDispensed() } - is MainNavigation.PrescriptionsRedeemed -> - coScope.launch { prescriptionViewModel.onSelectDispensed() } - } - } - - if (state.prescriptions.isNotEmpty() && selectedPrescription != null) { - HorizontalSplittable( - split = 0.3f, - contentLeft = { - PrescriptionList( - state.prescriptions, - selectedPrescription = selectedPrescription.prescription, - onClickPrescription = { - coScope.launch { - prescriptionViewModel.onSelectPrescription( - it - ) - } - } - ) - }, - contentRight = { - PrescriptionDetailsScreen( - navigation = navigation, - prescription = selectedPrescription, - audits = state.selectedPrescriptionAudits, - onClickDelete = { - prescriptionViewModel.deletePrescription(selectedPrescription.prescription) - } - ) - } - ) - } - } -} - -@Composable -private fun DeleteAlertDialog( - prescriptionViewModel: PrescriptionViewModel -) { - var deleteState by remember { mutableStateOf(null) } - - LaunchedEffect(Unit) { - prescriptionViewModel.deleteState().collect { - deleteState = it - } - } - deleteState?.let { - Dialog( - title = it.error ?: "", - confirmButton = { - TextButton(onClick = { - deleteState = null - }) { - Text(App.strings.cancel().uppercase(Locale.getDefault())) - } - }, - onDismissRequest = { deleteState = null } - ) - } -} - -@Composable -private fun PrescriptionList( - prescriptions: List, - selectedPrescription: PrescriptionUseCaseData.Prescription, - onClickPrescription: (PrescriptionUseCaseData.Prescription) -> Unit -) { - val formatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - - val lazyListState = rememberLazyListState() - val scrollbarAdapter = rememberScrollbarAdapter(lazyListState) - - Box(Modifier.fillMaxSize()) { - LazyColumn( - state = lazyListState - ) { - itemsIndexed(prescriptions, key = { _, it -> it }) { _, it -> - Prescription( - modifier = Modifier.fillMaxWidth(), - name = it.name ?: App.strings.desktopPrescriptionNoData(), - selected = it.taskId == selectedPrescription.taskId, - expiresOnText = expiresOrAcceptedUntil(it), - prescribedOnText = App.strings.desktopPrescriptionPrescribedOn( - count = 1, - it.authoredOn.format(formatter) - ), - onClick = { onClickPrescription(it) } - ) - HorizontalDivider() - } - } - VerticalScrollbar( - scrollbarAdapter, - modifier = Modifier.align(Alignment.CenterEnd).padding(horizontal = 1.dp).fillMaxHeight() - ) - } -} - -@Composable -fun expiresOrAcceptedUntil( - prescription: PrescriptionUseCaseData.Prescription -): String { - val now = remember { LocalDate.now().toEpochDay() } - val expiryDaysLeft = prescription.expiresOn.toEpochDay() - now - val acceptDaysLeft = prescription.acceptUntil.toEpochDay() - now - - return when { - prescription.redeemedOn != null -> { - val dtFormatter = - remember(prescription) { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - prescription.redeemedOn.format(dtFormatter) - } - acceptDaysLeft >= 0 -> App.strings.desktopPrescriptionAcceptUntil( - count = acceptDaysLeft.toInt(), - acceptDaysLeft - ) - expiryDaysLeft >= 0 -> App.strings.desktopPrescriptionExpiresOn(count = expiryDaysLeft.toInt(), expiryDaysLeft) - else -> App.strings.desktopPrescriptionExpired() - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun Prescription( - modifier: Modifier, - name: String, - selected: Boolean, - expiresOnText: String, - prescribedOnText: String, - onClick: () -> Unit -) { - var hovered by remember { mutableStateOf(false) } - - val alpha by animateFloatAsState(if (hovered) 1f else 0f) - val color = if (selected) AppTheme.colors.primary100 else AppTheme.colors.neutral100.copy(alpha = alpha) - - Column( - modifier - .fillMaxSize() - .background(color) - .clickable(onClick = onClick) - .padding(PaddingDefaults.Medium) - .pointerMoveFilter( - onEnter = { - hovered = true - false - }, - onExit = { - hovered = false - false - } - ) - ) { - Text(name, style = MaterialTheme.typography.subtitle1) - Text(expiresOnText, style = AppTheme.typography.body2l) - SpacerSmall() - Text(prescribedOnText, style = AppTheme.typography.captionl) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt deleted file mode 100644 index 96c3ccc2..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.onSubscription -import kotlinx.coroutines.flow.transform -import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.kodein.di.bindings.ScopeCloseable - -class PrescriptionViewModel( - private val dispatchProvider: DispatchProvider, - private val prescriptionUseCase: PrescriptionUseCase -) : ScopeCloseable { - private val deleteScope = CoroutineScope(dispatchProvider.io) - private val deleteResult = MutableSharedFlow>() - private val selectedPrescription = MutableSharedFlow() - private val prescriptionType = MutableStateFlow(PrescriptionUseCase.PrescriptionType.NotDispensed) - - val defaultState = - PrescriptionScreenData.State(emptyList(), PrescriptionUseCase.PrescriptionType.NotDispensed, null, emptyList()) - - @OptIn(ExperimentalCoroutinesApi::class) - fun screenState(): Flow { - return prescriptionType.flatMapLatest { type -> - prescriptionUseCase.prescriptions(type).flatMapLatest { prescriptions -> - selectedPrescription.onSubscription { - emit(prescriptions.firstOrNull()?.taskId) - }.transformLatest { selected -> - if (selected == null) { - emit( - PrescriptionScreenData.State( - prescriptions = prescriptions, - prescriptionsType = type, - selectedPrescription = null, - selectedPrescriptionAudits = emptyList() - ) - ) - } else { - emitAll( - combine( - prescriptionUseCase.prescriptionDetails(selected), - prescriptionUseCase.audits(selected) - ) { details, audits -> - PrescriptionScreenData.State( - prescriptions = prescriptions, - prescriptionsType = type, - selectedPrescription = details, - selectedPrescriptionAudits = audits - ) - } - ) - } - } - } - } - } - - fun deleteState() = deleteResult.transform { - if (it.isFailure) { - it.exceptionOrNull()?.let { - emit(PrescriptionScreenData.DeleteState(it.message)) - } - } - } - - suspend fun onSelectPrescription(prescription: PrescriptionUseCaseData.Prescription) { - selectedPrescription.emit(prescription.taskId) - } - - suspend fun onSelectDispensed() { - prescriptionType.emit(PrescriptionUseCase.PrescriptionType.Dispensed) - } - - suspend fun onSelectNotDispensed() { - prescriptionType.emit(PrescriptionUseCase.PrescriptionType.NotDispensed) - } - - suspend fun update() = withContext(dispatchProvider.io) { - prescriptionUseCase.update() - } - - fun deletePrescription(prescription: PrescriptionUseCaseData.Prescription) { - deleteScope.launch { - val r = prescriptionUseCase.delete(prescription.taskId) - if (r.isSuccess) { - prescriptionUseCase.update() - } - deleteResult.emit(r) - } - } - - override fun close() { - deleteScope.cancel() - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt deleted file mode 100644 index b9961507..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui.model - -import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData - -object PrescriptionScreenData { - @Immutable - data class State( - val prescriptions: List, - val prescriptionsType: PrescriptionUseCase.PrescriptionType, - val selectedPrescription: PrescriptionUseCaseData.PrescriptionDetails?, - val selectedPrescriptionAudits: List - ) - - @Immutable - data class DeleteState( - val error: String? - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionMapper.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionMapper.kt deleted file mode 100644 index fffe4dce..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionMapper.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.usecase - -import de.gematik.ti.erp.app.fhir.extractInsurance -import de.gematik.ti.erp.app.fhir.extractMedication -import de.gematik.ti.erp.app.fhir.extractMedicationRequest -import de.gematik.ti.erp.app.fhir.extractOrganization -import de.gematik.ti.erp.app.fhir.extractPatient -import de.gematik.ti.erp.app.fhir.extractPractitioner -import de.gematik.ti.erp.app.prescription.repository.model.SimpleMedicationDispense -import de.gematik.ti.erp.app.prescription.repository.model.SimpleTask -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import java.time.LocalDate - -class PrescriptionMapper { - fun mapSimpleTask(task: SimpleTask, redeemedOn: LocalDate?) = - PrescriptionUseCaseData.Prescription( - taskId = task.taskId, - name = task.medicationText, - expiresOn = task.expiresOn, - acceptUntil = task.acceptUntil, - organization = task.organization, - authoredOn = task.authoredOn, - redeemedOn = redeemedOn - ) - - fun mapSimpleTaskDetailed(task: SimpleTask, dispenses: List) = - PrescriptionUseCaseData.PrescriptionDetails( - prescription = mapSimpleTask(task, dispenses.firstOrNull()?.whenHandedOver?.toLocalDate()), - patient = requireNotNull(task.rawKBVBundle.extractPatient()), - practitioner = requireNotNull(task.rawKBVBundle.extractPractitioner()), - medication = requireNotNull(task.rawKBVBundle.extractMedication()), - medicationDispenses = dispenses.map { mapSimpleMedicationDispense(it) }, - insurance = requireNotNull(task.rawKBVBundle.extractInsurance()), - organization = requireNotNull(task.rawKBVBundle.extractOrganization()), - medicationRequest = requireNotNull(task.rawKBVBundle.extractMedicationRequest()) - ) - - fun mapSimpleMedicationDispense(dispense: SimpleMedicationDispense) = - PrescriptionUseCaseData.MedicationDispense( - medication = dispense.medicationDetail, - dosageInstruction = dispense.dosageInstruction - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt deleted file mode 100644 index 92af2a5e..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.usecase - -import com.google.zxing.BarcodeFormat -import com.google.zxing.common.BitMatrix -import com.google.zxing.datamatrix.DataMatrixWriter -import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository -import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent -import de.gematik.ti.erp.app.prescription.repository.model.SimpleMedicationDispense -import de.gematik.ti.erp.app.prescription.repository.model.SimpleTask -import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.combineTransform -import kotlinx.coroutines.flow.map -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.putJsonArray - -// TODO: Break into smaller usecases -class PrescriptionUseCase( - private val repository: DesktopPrescriptionRepository, - private val mapper: PrescriptionMapper -) { - enum class PrescriptionType { - NotDispensed, Dispensed - } - - fun tasks(): Flow> = repository.tasks() - - fun prescriptions( - type: PrescriptionType = PrescriptionType.NotDispensed - ): Flow> = - combine(tasks(), medicationDispenses()) { tasks, dispenses -> - val dispenseIds = dispenses.map { it.taskId } - tasks - .filter { - when (type) { - PrescriptionType.NotDispensed -> it.taskId !in dispenseIds - PrescriptionType.Dispensed -> it.taskId in dispenseIds - } - } - .sortedByDescending { it.authoredOn } - .map { task -> - when (type) { - PrescriptionType.NotDispensed -> mapper.mapSimpleTask(task, null) - PrescriptionType.Dispensed -> { - val d = dispenses.find { task.taskId == it.taskId } - mapper.mapSimpleTask(task, d?.whenHandedOver?.toLocalDate()) - } - } - } - } - - fun prescriptionDetails(taskId: String): Flow = - combineTransform(tasks(), medicationDispenses()) { tasks, dispenses -> - tasks - .find { it.taskId == taskId } - ?.let { task -> - val d = dispenses.filter { task.taskId == it.taskId } - emit(mapper.mapSimpleTaskDetailed(task, d)) - } - } - - fun auditEvents(): Flow> = repository.auditEvents() - - fun audits(taskId: String): Flow> = - auditEvents().map { events -> - events - .filter { it.taskId == taskId } - .map { - PrescriptionUseCaseData.PrescriptionAudit( - text = it.text, - timestamp = it.timestamp - ) - } - } - - fun medicationDispenses(): Flow> = repository.medicationDispenses() - - suspend fun update() = repository.download() - suspend fun delete(taskId: String) = repository.delete(taskId) -} - -fun createMatrixCode(taskId: String, accessCode: String): BitMatrix { - return DataMatrixWriter().encode(buildDataMatrixPayload(taskId, accessCode), BarcodeFormat.DATA_MATRIX, 1, 1) -} - -private fun buildDataMatrixPayload(taskId: String, accessCode: String): String = - buildJsonObject { - putJsonArray("urls") { - add("Task/$taskId/\$accept?ac=$accessCode") - } - }.toString().replace("\\", "") diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt deleted file mode 100644 index 2ff4545d..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.usecase.model - -import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.fhir.InsuranceCompanyDetail -import de.gematik.ti.erp.app.fhir.MedicationDetail -import de.gematik.ti.erp.app.fhir.MedicationRequestDetail -import de.gematik.ti.erp.app.fhir.OrganizationDetail -import de.gematik.ti.erp.app.fhir.PatientDetail -import de.gematik.ti.erp.app.fhir.PractitionerDetail -import java.time.LocalDate -import java.time.LocalDateTime - -object PrescriptionUseCaseData { - @Stable - data class Prescription( - val taskId: String, - val name: String?, - val organization: String, - val authoredOn: LocalDateTime, - val expiresOn: LocalDate, - val acceptUntil: LocalDate, - val redeemedOn: LocalDate? - ) - - @Stable - data class MedicationDispense( - val medication: MedicationDetail, - val dosageInstruction: String? - ) - - @Stable - data class PrescriptionDetails( - val prescription: Prescription, - val patient: PatientDetail, - val practitioner: PractitionerDetail, - val medication: MedicationDetail, - val medicationDispenses: List, - val insurance: InsuranceCompanyDetail, - val organization: OrganizationDetail, - val medicationRequest: MedicationRequestDetail - ) { - val isDispensed = medicationDispenses.isNotEmpty() - - // TODO account for multiple dispenses - val isSubstituted = - isDispensed && medication.uniqueIdentifier != medicationDispenses.first().medication.uniqueIdentifier - - // how to apply/consume the medication - fun dosageInstruction(): String? = - if (isSubstituted) { - // TODO see above - medicationDispenses.first().dosageInstruction - } else { - medicationRequest.dosageInstruction - } - - // what kind of dosage; like pills, creme, ... - fun medicationType(): String? = - if (isSubstituted) { - // TODO see above - medicationDispenses.first().medication.dosageCode - } else { - medication.dosageCode - } - - // PZN - fun uniqueIdentifier(): String? = - if (isSubstituted) { - // TODO see above - medicationDispenses.first().medication.uniqueIdentifier - } else { - medication.uniqueIdentifier - } - } - - @Stable - data class PrescriptionAudit( - val text: String?, - val timestamp: LocalDateTime - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/di/ProtocolModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/di/ProtocolModule.kt deleted file mode 100644 index c13ae6b4..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/di/ProtocolModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.di - -import de.gematik.ti.erp.app.protocol.repository.model.ProtocolRepository -import de.gematik.ti.erp.app.protocol.usecase.ProtocolUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.bindings.Scope -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton - -fun protocolModule(scope: Scope) = DI.Module("Protocol Module") { - bind { scoped(scope).singleton { ProtocolRepository(instance(), instance()) } } - bind { scoped(scope).singleton { ProtocolUseCase(instance()) } } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/repository/model/ProtocolRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/repository/model/ProtocolRepository.kt deleted file mode 100644 index 92cd651f..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/repository/model/ProtocolRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.repository.model - -import de.gematik.ti.erp.app.api.ErpService -import de.gematik.ti.erp.app.core.safeApiCall -import de.gematik.ti.erp.app.fhir.FhirMapper -import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent - -class ProtocolRepository( - private val service: ErpService, - private val mapper: FhirMapper -) { - suspend fun downloadAuditEvents( - offset: Int, - count: Int - ): Result> = - safeApiCall("Error getting audit events: offset $offset - count $count") { - service.getAllAuditEvents( - offset = offset, - count = count - ) - }.mapCatching { - mapper.mapFhirBundleToAuditEvents(it) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolScreen.kt deleted file mode 100644 index 610b43fa..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolScreen.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.VerticalScrollbar -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollbarAdapter -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.rememberScope -import org.kodein.di.bind -import org.kodein.di.compose.rememberInstance -import org.kodein.di.compose.subDI -import org.kodein.di.instance -import org.kodein.di.scoped -import org.kodein.di.singleton -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.items -import de.gematik.ti.erp.app.common.SpacerTiny -import de.gematik.ti.erp.app.common.theme.AppTheme -import de.gematik.ti.erp.app.common.theme.PaddingDefaults -import de.gematik.ti.erp.app.protocol.usecase.ProtocolUseCaseData -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -fun ProtocolScreen() { - val scope = rememberScope() - - subDI(diBuilder = { - bind { scoped(scope).singleton { ProtocolViewModel(instance()) } } - }) { - val protocolViewModel by rememberInstance() - - val searchPagingItems = protocolViewModel.protocolSearchFlow.collectAsLazyPagingItems() - - val lazyListState = rememberLazyListState() - val scrollbarAdapter = rememberScrollbarAdapter(lazyListState) - - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colors.background - ) { - Box { - LazyColumn( - state = lazyListState, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), - contentPadding = PaddingValues(vertical = PaddingDefaults.Medium), - modifier = Modifier.widthIn(max = 560.dp).align(Alignment.Center) - ) { - items(searchPagingItems) { item -> - item?.run { ProtocolEntry(item) } - } - } - VerticalScrollbar( - scrollbarAdapter, - modifier = Modifier.align(Alignment.CenterEnd).padding(horizontal = 1.dp).fillMaxHeight() - ) - } - } - } -} - -@Composable -private fun ProtocolEntry( - protocolEntry: ProtocolUseCaseData.ProtocolEntry -) { - Card( - border = BorderStroke(1.dp, AppTheme.colors.neutral300), - elevation = 0.dp, - shape = RoundedCornerShape(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.padding(PaddingDefaults.Medium)) { - Text(protocolEntry.text, style = MaterialTheme.typography.body2) - SpacerTiny() - Text( - phrasedDateString(date = protocolEntry.timestamp), - style = AppTheme.typography.body2l - ) - } - } -} - -@Composable -private fun phrasedDateString(date: LocalDateTime): String { - val dateFormatter = remember { DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG) } - - return date.atZone(ZoneId.systemDefault()).format(dateFormatter) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolViewModel.kt deleted file mode 100644 index 973c458c..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/ProtocolViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.ui - -import de.gematik.ti.erp.app.protocol.usecase.ProtocolUseCase - -class ProtocolViewModel( - protocolUseCase: ProtocolUseCase -) { - val protocolSearchFlow = protocolUseCase.loadProtocol() -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/model/ProtocolScreenData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/model/ProtocolScreenData.kt deleted file mode 100644 index ac97d875..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/ui/model/ProtocolScreenData.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.ui.model - -class ProtocolScreenData diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCase.kt deleted file mode 100644 index 4ad22fd2..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCase.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.usecase - -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.PagingSource -import androidx.paging.PagingState -import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent -import de.gematik.ti.erp.app.protocol.repository.model.ProtocolRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlin.math.max - -class ProtocolUseCase( - private val repository: ProtocolRepository -) { - data class AuditPagingKey(val offset: Int) - - inner class PharmacyPagingSource : PagingSource() { - - override fun getRefreshKey( - state: PagingState - ): AuditPagingKey? = null - - override suspend fun load(params: LoadParams): - LoadResult { - val count = params.loadSize - val key = params.key ?: AuditPagingKey(0) - - val resultSearchBundle = - repository.downloadAuditEvents(offset = key.offset, count = count) - - return resultSearchBundle.fold( - onSuccess = { events -> - val nextKey = if (events.size == count) { - AuditPagingKey( - key.offset + events.size - ) - } else { - null - } - val prevKey = if (key.offset == 0) null else key.copy(offset = max(0, key.offset - count)) - - LoadResult.Page( - data = mapEvents(events), - nextKey = nextKey, - prevKey = prevKey, - itemsBefore = if (prevKey != null) count else 0, - itemsAfter = if (nextKey != null) count else 0 - ) - }, - onFailure = { - LoadResult.Error(it) - } - ) - } - } - - fun loadProtocol(): Flow> = - Pager( - PagingConfig( - pageSize = 10, - initialLoadSize = 50, - maxSize = 50 * 2 - ), - pagingSourceFactory = { PharmacyPagingSource() } - ).flow - - private fun mapEvents(events: List): List = - events.map { - ProtocolUseCaseData.ProtocolEntry( - text = it.text, - timestamp = it.timestamp, - taskId = it.taskId - ) - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCaseData.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCaseData.kt deleted file mode 100644 index c494c0d5..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/ProtocolUseCaseData.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.protocol.usecase - -import androidx.compose.runtime.Immutable -import java.time.LocalDateTime - -object ProtocolUseCaseData { - @Immutable - data class ProtocolEntry( - val text: String, - val timestamp: LocalDateTime, - val taskId: String - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt deleted file mode 100644 index 3f2c93f1..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/Bytes.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils - -import java.math.BigInteger - -object Bytes { - private const val PAD = 0x80.toByte() - - /** - * Padding the data with [PAD]. - * - * @param data byte array with data - * @param blockSize int - * @return byte array with padded data - */ - fun padData(data: ByteArray, blockSize: Int): ByteArray = - ByteArray(data.size + (blockSize - data.size % blockSize)).apply { - data.copyInto(this) - this[data.size] = PAD - } - - /** - * Unpadding the data. - * - * @param paddedData byte array with padded data - * @return byte array with data - */ - fun unPadData(paddedData: ByteArray): ByteArray { - for (i in paddedData.indices.reversed()) { - if (paddedData[i] == PAD) { - return paddedData.copyOfRange(0, i) - } - } - return paddedData - } - - /** - * Converts a BigInteger into a ByteArray. A leading byte with the value 0 is truncated. - * - * @param bigInteger The BigInteger object to convert. - * @return The ByteArray without leading 0-byte - */ - fun bigIntToByteArray(bigInteger: BigInteger): ByteArray { - val bigIntArray = bigInteger.toByteArray() - return if (bigIntArray[0] == 0.toByte()) { - bigIntArray.copyOfRange(1, bigIntArray.size) - } else { - bigIntArray - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/CryptoUtils.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/CryptoUtils.kt deleted file mode 100644 index d12eeb81..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/CryptoUtils.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils - -import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.crypto.prng.FixedSecureRandom -import org.bouncycastle.crypto.prng.SP800SecureRandomBuilder -import java.security.SecureRandom -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey - -fun generateRandomAES256Key(seed: ByteArray): SecretKey = - KeyGenerator.getInstance("AES").apply { - init(256, secureRandomInstance(seed)) - }.generateKey() - -fun secureRandomInstance(seed: ByteArray): SecureRandom = - SP800SecureRandomBuilder(FixedSecureRandom(seed), false) - .buildHash(SHA256Digest(), seed, false) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/FhirUtils.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/FhirUtils.kt deleted file mode 100644 index 000e79d0..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/FhirUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.utils - -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.ZoneId -import java.util.Date - -fun Date.convertFhirDateToLocalDateTime(): LocalDateTime = - LocalDateTime.ofInstant(toInstant(), ZoneId.systemDefault()) - -fun Date.convertFhirDateToLocalDate(): LocalDate = - LocalDate.ofInstant(toInstant(), ZoneId.systemDefault()) diff --git a/desktop/src/jvmMain/resources/images/calling_lady.webp b/desktop/src/jvmMain/resources/images/calling_lady.webp deleted file mode 100644 index b245e1e2..00000000 Binary files a/desktop/src/jvmMain/resources/images/calling_lady.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/card_wall_card_can.webp b/desktop/src/jvmMain/resources/images/card_wall_card_can.webp deleted file mode 100644 index 1db2e872..00000000 Binary files a/desktop/src/jvmMain/resources/images/card_wall_card_can.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/card_wall_man.webp b/desktop/src/jvmMain/resources/images/card_wall_man.webp deleted file mode 100644 index 075a1967..00000000 Binary files a/desktop/src/jvmMain/resources/images/card_wall_man.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/clapping_hands.webp b/desktop/src/jvmMain/resources/images/clapping_hands.webp deleted file mode 100644 index 5fec1ce6..00000000 Binary files a/desktop/src/jvmMain/resources/images/clapping_hands.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/clapping_hands_hint_yellow.png b/desktop/src/jvmMain/resources/images/clapping_hands_hint_yellow.png deleted file mode 100644 index 74622938..00000000 Binary files a/desktop/src/jvmMain/resources/images/clapping_hands_hint_yellow.png and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/crew.webp b/desktop/src/jvmMain/resources/images/crew.webp deleted file mode 100644 index e3a32dcc..00000000 Binary files a/desktop/src/jvmMain/resources/images/crew.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/doctor_circle.webp b/desktop/src/jvmMain/resources/images/doctor_circle.webp deleted file mode 100644 index a9926f71..00000000 Binary files a/desktop/src/jvmMain/resources/images/doctor_circle.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/erp_logo.webp b/desktop/src/jvmMain/resources/images/erp_logo.webp deleted file mode 100644 index d1ba9314..00000000 Binary files a/desktop/src/jvmMain/resources/images/erp_logo.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/erp_logo_dev.webp b/desktop/src/jvmMain/resources/images/erp_logo_dev.webp deleted file mode 100644 index 0bee00a5..00000000 Binary files a/desktop/src/jvmMain/resources/images/erp_logo_dev.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/health_card_hint_blue.webp b/desktop/src/jvmMain/resources/images/health_card_hint_blue.webp deleted file mode 100644 index 13a88f05..00000000 Binary files a/desktop/src/jvmMain/resources/images/health_card_hint_blue.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/ic_info.webp b/desktop/src/jvmMain/resources/images/ic_info.webp deleted file mode 100644 index 59a5b2d1..00000000 Binary files a/desktop/src/jvmMain/resources/images/ic_info.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/ic_onboarding_logo_flag.xml b/desktop/src/jvmMain/resources/images/ic_onboarding_logo_flag.xml deleted file mode 100644 index 5f316ff9..00000000 --- a/desktop/src/jvmMain/resources/images/ic_onboarding_logo_flag.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/desktop/src/jvmMain/resources/images/ic_onboarding_logo_gematik.xml b/desktop/src/jvmMain/resources/images/ic_onboarding_logo_gematik.xml deleted file mode 100644 index 6901458d..00000000 --- a/desktop/src/jvmMain/resources/images/ic_onboarding_logo_gematik.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/desktop/src/jvmMain/resources/images/information.webp b/desktop/src/jvmMain/resources/images/information.webp deleted file mode 100644 index 549a8a11..00000000 Binary files a/desktop/src/jvmMain/resources/images/information.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/laptop_woman_blue.webp b/desktop/src/jvmMain/resources/images/laptop_woman_blue.webp deleted file mode 100644 index b3f4b0c7..00000000 Binary files a/desktop/src/jvmMain/resources/images/laptop_woman_blue.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/laptop_woman_yellow.webp b/desktop/src/jvmMain/resources/images/laptop_woman_yellow.webp deleted file mode 100644 index 9edba405..00000000 Binary files a/desktop/src/jvmMain/resources/images/laptop_woman_yellow.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/medical_hand_out_circle_blue.webp b/desktop/src/jvmMain/resources/images/medical_hand_out_circle_blue.webp deleted file mode 100644 index 96c79e0e..00000000 Binary files a/desktop/src/jvmMain/resources/images/medical_hand_out_circle_blue.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/medical_hand_out_circle_red.webp b/desktop/src/jvmMain/resources/images/medical_hand_out_circle_red.webp deleted file mode 100644 index bd043ff2..00000000 Binary files a/desktop/src/jvmMain/resources/images/medical_hand_out_circle_red.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/nights_stay.svg b/desktop/src/jvmMain/resources/images/nights_stay.svg deleted file mode 100644 index 0a94a7b0..00000000 --- a/desktop/src/jvmMain/resources/images/nights_stay.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/desktop/src/jvmMain/resources/images/oh_no.webp b/desktop/src/jvmMain/resources/images/oh_no.webp deleted file mode 100644 index e12d41bd..00000000 Binary files a/desktop/src/jvmMain/resources/images/oh_no.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/oh_no_girl_hint_red.webp b/desktop/src/jvmMain/resources/images/oh_no_girl_hint_red.webp deleted file mode 100644 index 828d9cc4..00000000 Binary files a/desktop/src/jvmMain/resources/images/oh_no_girl_hint_red.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/onboarding_boygrannygranpa.webp b/desktop/src/jvmMain/resources/images/onboarding_boygrannygranpa.webp deleted file mode 100644 index 97bc210e..00000000 Binary files a/desktop/src/jvmMain/resources/images/onboarding_boygrannygranpa.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/onboarding_healthcard.webp b/desktop/src/jvmMain/resources/images/onboarding_healthcard.webp deleted file mode 100644 index 04c493b0..00000000 Binary files a/desktop/src/jvmMain/resources/images/onboarding_healthcard.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/onboarding_pharmacist.webp b/desktop/src/jvmMain/resources/images/onboarding_pharmacist.webp deleted file mode 100644 index cb792d63..00000000 Binary files a/desktop/src/jvmMain/resources/images/onboarding_pharmacist.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/pharmacist.webp b/desktop/src/jvmMain/resources/images/pharmacist.webp deleted file mode 100644 index 2a8368d9..00000000 Binary files a/desktop/src/jvmMain/resources/images/pharmacist.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/pharmacist_2.webp b/desktop/src/jvmMain/resources/images/pharmacist_2.webp deleted file mode 100644 index c7ea0844..00000000 Binary files a/desktop/src/jvmMain/resources/images/pharmacist_2.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/pharmacist_circle.webp b/desktop/src/jvmMain/resources/images/pharmacist_circle.webp deleted file mode 100644 index 37d2ad4b..00000000 Binary files a/desktop/src/jvmMain/resources/images/pharmacist_circle.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/pharmacist_hint.webp b/desktop/src/jvmMain/resources/images/pharmacist_hint.webp deleted file mode 100644 index 3b996cbb..00000000 Binary files a/desktop/src/jvmMain/resources/images/pharmacist_hint.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/pharmacist_with_phone_hint_blue.png b/desktop/src/jvmMain/resources/images/pharmacist_with_phone_hint_blue.png deleted file mode 100644 index f89734a4..00000000 Binary files a/desktop/src/jvmMain/resources/images/pharmacist_with_phone_hint_blue.png and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/wb_sunny.svg b/desktop/src/jvmMain/resources/images/wb_sunny.svg deleted file mode 100644 index aec15fdc..00000000 --- a/desktop/src/jvmMain/resources/images/wb_sunny.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_blue.webp b/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_blue.webp deleted file mode 100644 index 09435aa2..00000000 Binary files a/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_blue.webp and /dev/null differ diff --git a/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_red.webp b/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_red.webp deleted file mode 100644 index 058d5e93..00000000 Binary files a/desktop/src/jvmMain/resources/images/woman_red_shirt_circle_red.webp and /dev/null differ diff --git a/gradle.properties b/gradle.properties index a731793d..4be6e0e2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -#Mon Jan 22 11:48:57 CET 2024 +#Fri Dec 13 09:17:13 CET 2024 APP_CENTER_SECRET=APP_CENTER_SECRET APP_TRUST_ANCHOR_BASE64=MIICaTCCAg+gAwIBAgIBATAKBggqhkjOPQQDAjBtMQswCQYDVQQGEwJERTEVMBMGA1UECgwMZ2VtYXRpayBHbWJIMTQwMgYDVQQLDCtaZW50cmFsZSBSb290LUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMREwDwYDVQQDDAhHRU0uUkNBMzAeFw0xNzEwMTcwNzEzMDNaFw0yNzEwMTUwNzEzMDNaMG0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxnZW1hdGlrIEdtYkgxNDAyBgNVBAsMK1plbnRyYWxlIFJvb3QtQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxETAPBgNVBAMMCEdFTS5SQ0EzMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABFhZKSE0xvaeHzZB0A7sRYwIphEWYk/+uFw4kOLnBt2kbP4P7L0lFOQfp6W0a2lCcmKk+k25VHrj7PCMyV/AVdqjgZ4wgZswHQYDVR0OBBYEFN/DvnW+JesTMjAup1CFCJ83ENDoMEIGCCsGAQUFBwEBBDYwNDAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Aucm9vdC1jYS50aS1kaWVuc3RlLmRlL29jc3AwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwFQYDVR0gBA4wDDAKBggqghQATASBIzAKBggqhkjOPQQDAgNIADBFAiEAnOlHsQ5tQ2HPoKVngVQnbvVGteLyMSEnNGbYegnfPFECIEUlFmjATBNklr35xvWQPZUMdIsy7SzUulwFDodpdGr/ BASE_SERVICE_URI_PU=https\://erp.app.ti-dienste.de/ @@ -9,16 +9,27 @@ IDP_SERVICE_URI_PU=https\://idp.app.ti-dienste.de/.well-known/ MAPS_API_KEY=MAPS_API_KEY PHARMACY_SERVICE_URI=https\://apovzd.app.ti-dienste.de/ TEST_INSTRUMENTATION_ORCHESTRATOR=androidx.test.runner.AndroidJUnitRunner -USER_AGENT=eRp-App-Android/1.20.0 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.26.0 GMTIK VAU_OCSP_RESPONSE_MAX_AGE=12 -VERSION_CODE=3208 -VERSION_NAME=1.20.0 +VERSION_CODE=3652 +VERSION_NAME=1.26.0-RC1 +android.analyticsEnabled=false android.defaults.buildfeatures.buildconfig=true android.enableJetifier=false +android.experimental.r8.dex.vmoptions=-Xmx11g +android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 +android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect android.useAndroidX=true buildkonfig.flavor=googleTuInternal kotlin.code.style=official +kotlin.daemon.jvm.options="-Xmx11g" kotlin.jvm.target.validation.mode=IGNORE +kotlin.mpp.androidGradlePluginCompatibility.nowarn=true +kotlin.mpp.applyDefaultHierarchyTemplate=false kotlin.mpp.stability.nowarn=true -org.gradle.jvmargs=-Xmx4g -Dkotlin.daemon.jvm.options\="-Xmx4g" -XX\:+UseParallelGC +org.gradle.caching=true +org.gradle.configureondemand=true +org.gradle.jvmargs=-Xmx11g -XX\:+UseG1GC org.gradle.parallel=true +org.gradle.workers.max=6 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..61e8faa9 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,477 @@ +[versions] + +# android +agp = "8.2.2" +certificatetransparency-android = "2.5.42" +desugar = "2.1.3" + +# androidx +gradle-license-plugin = "0.9.7" +legacy-support-v4 = "1.0.0" +lifecycle = "2.8.7" +accompanist = "0.32.0" +activity-compose = "1.9.3" +markdown = "0.5.4" +appcompat = "1.7.0" +biometric = "1.2.0-alpha05" +camera = "1.4.0" +core-ktx = "1.15.0" +datastore-preferences = "1.1.1" +secrets-gradle-plugin = "2.0.1" +security-crypto = "1.1.0-alpha06" +window = "1.3.0" +webkit = "1.12.1" +androidx-work = "2.10.0" + +# androidx test +androidx-test-core = "1.6.1" +androidx-test-rules = "1.6.1" +androidx-test-junit = "1.2.1" +androidx-test-runner = "1.6.2" +androidx-test-services = "1.5.0" +androidx-test-espresso-core = "3.6.1" +androidx-test-arch-core = "2.2.0" + +# animation +lottie = "6.1.0" +shimmer = "1.2.0" + +# buildkonfig +buildkonfig-gradle-plugin = "0.15.1" + +# compose +# todo solve problems with compose update +compose-plugin = "1.6.1" +compose-foundation = "1.7.0-alpha02" +compose = "1.6.5" +compiler = "1.5.11" +navigation-compose = "2.7.7" +paging-compose = "3.2.1" +material3 = "1.2.1" + +# crypto +bouncycastle = "1.79" +jose4j = "0.9.5" +json = "20231013" + +# coroutines +coroutine-core = "1.9.0" + +# database +realm = "1.8.0" +realm-plugin = "1.7.1" + +# dependency check +dependency-check-gradle = "8.0.2" + +# design +# TODO: Snackbar breaks on update, needs fix +material = "1.12.0" + +# di +di = "7.20.2" + +# serialization +kotlinx-serialization = "1.9.23" +kotlinx-serialization-json = "1.6.3" + +# image +emoji2-emojipicker = "1.4.0" +# todo: remove as image-cropper is not maintained anymore +image-cropper = "4.3.2" +coil = "2.6.0" +barcode-scanning = "17.3.0" +zxing-core = "3.5.2" + +jetbrains-kotlin-jvm = "1.9.0" +kotlin-serilization = "1.9.23" +junit = "4.13.2" + +# kotlin +kotlin = "1.9.10" +atomicfu = "0.22.0" +kotlinx-datetime = "0.6.0-RC.2" +kotlinx-html = "0.11.0" +kotlin-reflect = "1.9.23" +kotlin-test = "1.9.23" + +# maps +maps-compose = "2.15.0" +maps-ktx = "4.0.0" +maps = "19.0.0" +maps-location = "21.3.0" + + +# logging +napier = "2.7.1" +slf4j = "2.0.9" + +# network +httpclient5 = "5.4" +retrofit = "2.9.0" +retrofit-kotlinx-serialization-converter = "1.0.0" +okhttp = "4.12.0" # this depends on the certificate transparancy library. Always check if they are compatible +okio = "3.9.0" + +# play +play-integrity = "1.4.0" +play-review = "2.0.2" +play-app-update = "2.1.0" + +# others +password-strength = "1.8.2" +reactive-state = "5.6.0" +opencsv = "5.5.2" +process-phoenix = "3.0.0" + +# tracking +tracking = "4.28.0" + +# testing +screenshot = "0.0.1-alpha07" +paparazzi = "1.3.3" +mockk = "1.13.10" +yaml = "2.3" + +# e2e testing +primsys = "0.9.1-SNAPSHOT" +kodeon = "0.1.1-SNAPSHOT" + +# Quality +# Always check if these libraries work on update +ktlint = "0.46.1" +ktlint-test = "0.45.2" +ktlint-core = "0.45.2" +detekt = "1.23.6" +composerules = "0.0.26" +rules = "1.0" + +# Plugins +kotlin-multiplatform = "1.9.23" +uiautomator-v18 = "2.2.0-alpha1" + +[libraries] + +# network +certificatetransparency-android = { module = "com.appmattus.certificatetransparency:certificatetransparency-android", version.ref = "certificatetransparency-android" } +network-httpclient5 = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "httpclient5" } +network-httpclient5-fluent = { module = "org.apache.httpcomponents.client5:httpclient5-fluent", version.ref = "httpclient5" } + +# Quality +quality-ktlint = { module = "com.pinterest:ktlint", version.ref = "ktlint" } +quality-ktlint-core = { module = "com.pinterest.ktlint:ktlint-core", version.ref = "ktlint-core" } +quality-ktlint-test = { module = "com.pinterest.ktlint:ktlint-test", version.ref = "ktlint-test" } +quality-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +quality-composerules = { module = "com.twitter.compose.rules:detekt", version.ref = "composerules" } +quality-rules = { module = "de.gematik.ti.erp.app:rules", version.ref = "rules" } + +# material +material = { module = "com.google.android.material:material", version.ref = "material" } + +# accompanist +accompanist-navigation-material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } +accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } +accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } +accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } +accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" } +accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } +accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +compose-markdown = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdown" } + +# androidx +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacy-support-v4" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore-preferences" } +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +androidx-window = { module = "androidx.window:window", version.ref = "window" } +androidx-work = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidx-work" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } +androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-junit" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-services" } +androidx-test-services = { module = "androidx.test.services:test-services", version.ref = "androidx-test-services" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" } +androidx-test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "androidx-test-espresso-core" } +androidx-test-arch-core = { module = "androidx.arch.core:core-testing", version.ref = "androidx-test-arch-core" } + +# bouncy castle +bouncycastle-jose4j = { module = "org.bitbucket.b_c:jose4j", version.ref = "jose4j" } +bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } + +# camera +camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } +camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } +camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } + +# compose +compose-plugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" } +compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" } +compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } + +# compose material +compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } +compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } + +# compose test +compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } + +# coroutine +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine-core" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutine-core" } +kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutine-core" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutine-core" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutine-core" } + +# coil +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } + +# di +di-compose = { module = "org.kodein.di:kodein-di-framework-compose", version.ref = "di" } +di-viewmodel = { module = "org.kodein.di:kodein-di-framework-android-x-viewmodel", version.ref = "di" } +di-savedstate = { module = "org.kodein.di:kodein-di-framework-android-x-viewmodel-savedstate", version.ref = "di" } +di-androidx = { module = "org.kodein.di:kodein-di-framework-android-x", version.ref = "di" } +di = { module = "org.kodein.di:kodein-di", version.ref = "di" } + +# google +barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcode-scanning" } +secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" } +zxing-core = { module = "com.google.zxing:core", version.ref = "zxing-core" } + +# kotlin +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } + +# kotlinx +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } +kotlinx-html-js = { module = "org.jetbrains.kotlinx:kotlinx-html-js", version.ref = "kotlinx-html" } +kotlinx-html-jvm = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinx-html" } +kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html", version.ref = "kotlinx-html" } + +# opencsv +opencsv = { module = "com.opencsv:opencsv", version.ref = "opencsv" } + +# life-cycle +lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } +lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } + +# logging +logging-napier = { module = "io.github.aakira:napier", version.ref = "napier" } +logging-slf4j-nop = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } + +# lottie +lottie = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } + +# maps +maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } +maps-ktx = { module = "com.google.maps.android:maps-ktx", version.ref = "maps-ktx" } +maps-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "maps-ktx" } +maps = { module = "com.google.android.gms:play-services-maps", version.ref = "maps" } +maps-location = { module = "com.google.android.gms:play-services-location", version.ref = "maps-location" } + +# mockk +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +test-mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } + +# navigation +navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } + +# network +network-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +network-okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +network-okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +# To work around a vulnerable Okio version 3.1.0 (CVE-2023-3635) we include a newer, non-vulnerable version +# to be selected by Gradle instead instead of the old one. +network-okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +network-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +network-retrofit-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization-converter" } + +# paging +paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging-compose" } +paging-common-ktx = { module = "androidx.paging:paging-common-ktx", version.ref = "paging-compose" } + +# password +password-strength = { module = "com.nulab-inc:zxcvbn", version.ref = "password-strength" } + +# play +play-intergrity = { module = "com.google.android.play:integrity", version.ref = "play-integrity" } +play-review = { module = "com.google.android.play:review-ktx", version.ref = "play-review" } +play-app-update = { module = "com.google.android.play:app-update-ktx", version.ref = "play-app-update" } + +# realm +database-realm = { module = "io.realm.kotlin:library-base", version.ref = "realm" } +database-realm-plugin = { module = "io.realm.kotlin:gradle-plugin", version.ref = "realm-plugin" } + +# reactive-state +reactive-state = { module = "com.ensody.reactivestate:reactivestate", version.ref = "reactive-state" } + +# serialization +json = { module = "org.json:json", version.ref = "json" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm", version.ref = "kotlinx-serialization-json" } + +# shimmer +shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "shimmer" } + +# tracking +tracking-library = { module = "com.contentsquare.android:library", version.ref = "tracking" } +tracking-compose = { module = "com.contentsquare.android:compose", version.ref = "tracking" } + +# others +desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } +process-phoenix = { module = "com.jakewharton:process-phoenix", version.ref = "process-phoenix" } +image-cropper = { module = "com.github.CanHub:Android-Image-Cropper", version.ref = "image-cropper" } +pdf-box = { module = "com.tom-roush:pdfbox-android", version = "2.0.27.0" } + +# testing +test-junit = { module = "junit:junit", version.ref = "junit" } +test-kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } +test-kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin-test" } +test-yaml = { module = "org.yaml:snakeyaml", version.ref = "yaml" } +test-turbine = { module = "app.cash.turbine:turbine", version = "1.1.0" } +# + +# e2e testing +primsys-client = { module = "de.gematik.test.erezept:primsys-rest-client", version.ref = "primsys" } +primsys-data = { module = "de.gematik.test.erezept:primsys-rest-data", version.ref = "primsys" } +kodeon-core = { module = "de.gematik.kodeon:kodeon-core", version.ref = "kodeon" } +kodeon-android = { module = "de.gematik.kodeon:kodeon-android", version.ref = "kodeon" } + +# project +buildkonfig-gradle-plugin = { module = "com.codingfeline.buildkonfig:buildkonfig-gradle-plugin", version.ref = "buildkonfig-gradle-plugin" } +android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } +kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-serilization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin-serilization" } +kotlin_atomicfu = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" } +dependency-check-gradle = { module = "org.owasp:dependency-check-gradle", version.ref = "dependency-check-gradle" } +gradle-license-plugin = { module = "com.jaredsburrows:gradle-license-plugin", version.ref = "gradle-license-plugin" } + +# tracing +tracing = { module = "androidx.tracing:tracing", version = "1.2.0" } +androidx-uiautomator-v18 = { group = "androidx.test.uiautomator", name = "uiautomator-v18", version.ref = "uiautomator-v18" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-multiplatform" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrains-kotlin-jvm" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinx-serialization" } +github-ben-manes-version = { id = "com.github.ben-manes.versions", version = "0.48.0" } +jaredburrows-license = { id = "com.jaredsburrows.license", version = "0.8.90" } +gradle-secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" } +realm-kotlin = { id = "io.realm.kotlin", version.ref = "realm-plugin" } +# compatibility check before upgrade: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#kotlin-compatibility +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig-gradle-plugin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot" } +paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" } + +# Bundles are used in VersionCatalogBundles file as a way to group dependencies together +[bundles] +accompanist = ["accompanist-swiperefresh", "accompanist-flowlayout", "accompanist-pager", + "accompanist-pager-indicators", "accompanist-systemuicontroller", "accompanist-navigation-material", + "accompanist-permission", "compose-markdown"] + +androidx-app = ["androidx-appcompat", "androidx-security-crypto"] + +androidx = ["androidx-appcompat", "androidx-legacy-support-v4", "androidx-core-ktx", "androidx-datastore-preferences", + "androidx-biometric", "androidx-webkit", "androidx-security-crypto", "androidx-activity-compose", "androidx-window"] + +androidx-test-arch-core = ["androidx-test-arch-core"] + +androidxtest = ["androidx-test-core", "androidx-test-rules", "androidx-test-junit", "androidx-test-runner", + "androidx-test-espresso-core", "androidx-test-espresso-intents"] + +androidxtestutils = ["androidx-test-orchestrator", "androidx-test-services"] + +animation = ["lottie", "shimmer"] + +compose-app = ["compose-runtime", "compose-foundation", "compose-ui-tooling", "compose-ui-tooling-preview"] + +compose = ["compose-compiler", "compose-animation", "compose-foundation", "compose-runtime", "compose-ui", + "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material", "compose-material3", + "compose-material-icons-core", "compose-material-icons-extended", "paging-compose", "paging-common-ktx", + "navigation-compose"] + +composetest = ["compose-ui-test", "compose-ui-test-manifest", "compose-ui-test-junit4"] + +camera = ["camera-camera2", "camera-lifecycle", "camera-view"] + +coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-coroutines-play-services"] + +corutinestest = ["kotlinx-coroutines-test"] + +crypto = ["bouncycastle-jose4j", "bouncycastle-bcprov", "bouncycastle-bcpkix"] + +cryptotest = ["bouncycastle-bcprov", "bouncycastle-bcpkix"] + +datetime = ["kotlinx-datetime"] + +datamatrix = ["barcode-scanning", "zxing-core"] + +database = ["database-realm"] + +di = ["di-compose", "di-viewmodel", "di-savedstate", "di-androidx", "di"] + +di-kotlin = ["di-compose", "di"] + +di-viewmodel = ["di-viewmodel", "di-savedstate"] + +image = ["coil-compose", "coil-gif"] + +kotlin = [ + # "kotlin-stdlib", + "kotlin-reflect"] + +kotlintest = ["test-kotlin-test", "test-kotlin-test-common", "kotlin-reflect"] + +lifecycle = ["lifecycle-viewmodel-ktx", "lifecycle-process", "lifecycle-runtime-compose"] + +logging = ["logging-napier", "logging-slf4j-nop"] + +maps = ["maps-compose", "maps-ktx", "maps-utils", "maps", "maps-location"] + +network = ["network-okhttp", "network-okhttp-logging", "network-okhttp-mockwebserver", "network-okio", + "network-retrofit", "network-retrofit-kotlinx-serialization-converter"] + +networktest = ["network-okhttp-mockwebserver", "network-okhttp"] + +others = ["image-cropper", "reactive-state", "password-strength"] + +pdfbox = ["pdf-box"] + +processphoenix = ["process-phoenix"] + +play = ["play-intergrity", "play-review", "play-app-update"] + +quality = ["quality-ktlint", "quality-detekt", "quality-composerules"] + +qualitydetektcomposerules = ["quality-composerules"] + +serialization = ["kotlinx-serialization-json", "kotlinx-serialization-core"] + +tracking = ["tracking-library", "tracking-compose"] + +testing = ["test-junit", "test-kotlin-test", "test-kotlin-test-common", + "test-yaml", "androidx-test-junit", "androidx-test-espresso-core", "json", "test-mockk-android"] + +testjunit = ["test-junit", "test-yaml", "json", "test-mockk"] + +mockandroid = ["test-mockk-android"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69aabc5d..1c23cfb4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Feb 04 22:24:01 CET 2024 +#Tue Jun 04 20:18:28 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt index 1af393b2..1de1723b 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("Unused") diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt index c5f16d69..8a623804 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("MemberNameEqualsClassName") @@ -24,8 +24,8 @@ object Dependencies { object Versions { object SdkVersions { const val MIN_SDK_VERSION = 26 - const val COMPILE_SDK_VERSION = 34 - const val TARGET_SDK_VERSION = 34 + const val COMPILE_SDK_VERSION = 35 + const val TARGET_SDK_VERSION = 35 } object JavaVersion { @@ -99,6 +99,7 @@ object Dependencies { const val camerax2 = "androidx.camera:camera-camera2:$camerax_version" const val cameraxLifecycle = "androidx.camera:camera-lifecycle:$camerax_version" const val cameraxView = "androidx.camera:camera-view:$camerax_version" + const val window = "androidx.window:window:1.2.0" object Test { private const val test_runner_version = "1.6.0-alpha04" @@ -169,7 +170,7 @@ object Dependencies { } object Crypto { - private const val json_web_token_version = "0.9.3" + private const val json_web_token_version = "0.9.5" private const val bouncy_castle_version = "1.76" const val jose4j = "org.bitbucket.b_c:jose4j:$json_web_token_version" @@ -197,7 +198,7 @@ object Dependencies { } object Datetime { - private const val datetime_version = "0.4.1" + private const val datetime_version = "0.6.0-RC.2" const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:$datetime_version" } @@ -217,7 +218,7 @@ object Dependencies { object GoogleMaps { private const val maps_version = "18.1.0" private const val maps_ktx_version = "4.0.0" - private const val location_version = "21.0.1" + private const val location_version = "21.1.0" private const val maps_compose_version = "2.15.0" // 3.1.1 needs core_ktx_version=1.12.0 val maps = gms("maps", maps_version) @@ -318,11 +319,10 @@ object Dependencies { } object Tracking { - private const val content_square_version = "4.22.0" + private const val content_square_version = "4.28.0" const val contentSquare = "com.contentsquare.android:library:$content_square_version" const val contentSquareCompose = "com.contentsquare.android:compose:$content_square_version" - const val contentSquareErrorAnalysis = "com.contentsquare.android:error-analysis:$content_square_version" } object Test { diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt index 756e62ed..4d90aa9c 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ @file:Suppress("TooManyFunctions") diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt index ae92b5ce..b2438ec9 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp diff --git a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/ResourceGenerationPlugin.kt b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/ResourceGenerationPlugin.kt index ad62191a..10ae4d3c 100644 --- a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/ResourceGenerationPlugin.kt +++ b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/ResourceGenerationPlugin.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp diff --git a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/networkSecurityConfigGen/AndroidNetworkConfigGeneratorTask.kt b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/networkSecurityConfigGen/AndroidNetworkConfigGeneratorTask.kt index b3e7cc31..c129460a 100644 --- a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/networkSecurityConfigGen/AndroidNetworkConfigGeneratorTask.kt +++ b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/networkSecurityConfigGen/AndroidNetworkConfigGeneratorTask.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.networkSecurityConfigGen diff --git a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/stringResGen/AndroidStringResourceGeneratorTask.kt b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/stringResGen/AndroidStringResourceGeneratorTask.kt index 350113cd..ddad8206 100644 --- a/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/stringResGen/AndroidStringResourceGeneratorTask.kt +++ b/plugins/resource-generation/src/main/kotlin/de/gematik/ti/erp/stringResGen/AndroidStringResourceGeneratorTask.kt @@ -1,22 +1,22 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ -@file:Suppress("ktlint:max-line-length", "MaxLineLength") +@file:Suppress("SpreadOperator", "ktlint:max-line-length", "MaxLineLength") package de.gematik.ti.erp.stringResGen diff --git a/plugins/technical-requirements-plugin/build.gradle.kts b/plugins/technical-requirements-plugin/build.gradle.kts index 1311fa40..5c06ccdb 100644 --- a/plugins/technical-requirements-plugin/build.gradle.kts +++ b/plugins/technical-requirements-plugin/build.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UseTomlInstead", "MagicNumber") + import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -5,12 +7,24 @@ plugins { `java-gradle-plugin` } -tasks.withType() { +tasks.withType { kotlinOptions.jvmTarget = "17" } +kotlin { + jvmToolchain(17) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + dependencies { implementation("com.android.tools.build:gradle:8.2.2") + implementation("org.jetbrains.kotlinx:kotlinx-html:0.11.0") + implementation("org.jsoup:jsoup:1.17.2") } gradlePlugin { diff --git a/plugins/technical-requirements-plugin/settings.gradle.kts b/plugins/technical-requirements-plugin/settings.gradle.kts index 240be2f2..abe075ef 100644 --- a/plugins/technical-requirements-plugin/settings.gradle.kts +++ b/plugins/technical-requirements-plugin/settings.gradle.kts @@ -7,7 +7,6 @@ pluginManagement { } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { google() mavenCentral() diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/Constants.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/Constants.kt new file mode 100644 index 00000000..238ca962 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/Constants.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("TopLevelPropertyNaming") + +package de.gematik.ti.erp.gradleplugins + +import de.gematik.ti.erp.gradleplugins.Regex.ANNOTATION_REGEX +import de.gematik.ti.erp.gradleplugins.model.RequirementData + +internal const val REQUIREMENTS_FILE = "requirements/requirements.properties" + +internal const val generateTechnicalRequirements = "generateTechnicalRequirements" +internal const val downloadBsiSpecs = "downloadBsiSpecs" +internal const val extractRequirements = "extractRequirements" +internal const val gutachterFolder = "gutachter" +internal const val ANDROID_APP_PATH = "/app/android/src/main/java/de/gematik/ti/erp/app" +internal const val APP_FEATURES_PATH = "/app/features" +internal const val SHARED_MODULE_PATH = "/common/src/commonMain/kotlin/de/gematik/ti/erp/app" +internal const val SHARED_TEST_MODULE_PATH = "/common/src/commonTest/kotlin/de/gematik/ti/erp/app" +internal const val SHARED_ANDROID_MODULE_PATH = "/common/src/androidMain/kotlin/de/gematik/ti/erp/app" +internal const val CODE_LINE = "codeLines =" +internal const val RATIONALE = "rationale =" +internal const val REQUIREMENTS_PATH = "requirements" +internal const val BSI_REQUIREMENTS_FILE_NAME = "bsi-requirements.html" +internal const val BSI_REQUIREMENTS_PATH = "requirements/bsi-requirements.html" +internal const val NO_LINK = "No link" + +// Add any other specification values that should be excluded here +val excludedSpecifications = setOf( + "gemSpec_eRp_FdV", + "BSI-eRp-ePA", + "gemF_Tokenverschlüsselung", + "gemSpec_IDP_Frontend", + "gemSpec_Krypt", + "unused", + "gemF_Biometrie", + "E-Rezept-App-Authentifizierungskonzept.pdf" +) + +object Regex { + val ANNOTATION_REGEX = Regex("""@Requirement\([^)]*\)""", RegexOption.MULTILINE) + val REQUIREMENT_REGEX = Regex("""\s*"([a-zA-Z_][a-zA-Z0-9_#-.]*)"\s*,?\s*""") + val SPEC_REGEX = Regex("""sourceSpecification\s*=\s*"(.*?)"""") + val RATIONALE_REGEX = Regex("""rationale\s*=\s*[^)]*""", RegexOption.MULTILINE) + val CODE_LINES_REGEX = Regex("""codeLines\s*=\s*[^)]*""", RegexOption.MULTILINE) + val QUOTES_REGEX = Regex(""""(.*?)"""") +} + +/** + * The requirement string is sanitized by removing the indexing part of it + */ +fun String.sanitizeRequirement(): String { + val regex = "(.*?)(?=#)|(.+)".toRegex() + val matchResult = regex.find(this) + return matchResult?.value ?: this +} + +fun String.removeAllRequirementAnnotations(): String = ANNOTATION_REGEX.replace(this, "") + +fun List.print() { + this.forEach { + println("-- Requirement --") + println("Header: ${it.header.requirement}") + it.body.forEach { body -> + println(body.requirement) + } + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt index 41bffbfd..d6c616e1 100644 --- a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt @@ -1,89 +1,228 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp.gradleplugins +import de.gematik.ti.erp.gradleplugins.Regex.ANNOTATION_REGEX +import de.gematik.ti.erp.gradleplugins.Regex.CODE_LINES_REGEX +import de.gematik.ti.erp.gradleplugins.Regex.QUOTES_REGEX +import de.gematik.ti.erp.gradleplugins.Regex.RATIONALE_REGEX +import de.gematik.ti.erp.gradleplugins.Regex.REQUIREMENT_REGEX +import de.gematik.ti.erp.gradleplugins.Regex.SPEC_REGEX +import de.gematik.ti.erp.gradleplugins.model.AnnotationBody +import de.gematik.ti.erp.gradleplugins.model.AnnotationHeader +import de.gematik.ti.erp.gradleplugins.model.CodeBlock +import de.gematik.ti.erp.gradleplugins.model.RequirementData +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import de.gematik.ti.erp.gradleplugins.provider.SpecificationSourceProvider +import de.gematik.ti.erp.gradleplugins.reports.ReportGenerator +import de.gematik.ti.erp.gradleplugins.reports.html.HtmlReportGenerator +import de.gematik.ti.erp.gradleplugins.repository.SourceSpecificationRepository +import de.gematik.ti.erp.gradleplugins.usecase.ExtractSourceSpecificationUseCase +import de.gematik.ti.erp.gradleplugins.usecase.GetSourceSpecificationUseCase import org.gradle.api.Plugin import org.gradle.api.Project - +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.Exec +import org.gradle.kotlin.dsl.register import java.io.File +import java.util.Properties class TechnicalRequirementsPlugin : Plugin { private var project: Project? = null + private var reportGenerator: ReportGenerator? = null + private val specificationSourceProvider = loadSpecificationSourceProvider() + + // Load the notes for all the requirements from the properties file + private val requirementProperties = Properties() + override fun apply(project: Project) { - this.project = project - project.tasks.register("generateTechnicalRequirementsMarkdown") { + // run ./gradlew :downloadBsiSpecs -Ptoken=YOUR_PRIVATE_TOKEN + project.tasks.register(downloadBsiSpecs) { + val downloadUrl = SpecificationSource.BSI_ERP_EPA.url + project.getToken()?.let { token -> + val outputFile = File( + "${project.rootProject.projectDir}/$REQUIREMENTS_PATH", + BSI_REQUIREMENTS_FILE_NAME + ) + commandLine("curl", "--header", "PRIVATE-TOKEN: $token", "-o", outputFile.absolutePath, downloadUrl) + doLast { + println("Downloaded HTML page saved to ${outputFile.absolutePath}") + } + } + } + + // run ./gradlew :generateTechnicalRequirements to generate the technical requirements file + project.tasks.register(generateTechnicalRequirements) { + init(project) + // load all the files that need to be analyzed doLast { val sourceDirs = setOf( - File(project.rootDir.path + "/app/android/src/main/java/de/gematik/ti/erp/app"), - File(project.rootDir.path + "/app/feature/src/main/kotlin/de/gematik/ti/erp/app"), - File(project.rootDir.path + "/common/src/commonMain/kotlin/de/gematik/ti/erp/app") + File(project.rootDir.path, ANDROID_APP_PATH), + File(project.rootDir, APP_FEATURES_PATH), + File(project.rootDir.path, SHARED_MODULE_PATH), + File(project.rootDir.path, SHARED_TEST_MODULE_PATH), + File(project.rootDir.path, SHARED_ANDROID_MODULE_PATH) ) - processSourceDirectory(sourceDirs) + try { + val requirements = generateRequirements(sourceDirs) + reportGenerator?.generate( + project = project, + requirements = requirements, + properties = requirementProperties, + specificationSourceProvider = specificationSourceProvider + )?.let { result -> + val reportFile = File( + "${project.rootProject.projectDir}", + "requirements-report.html" + ) + reportFile.writeText(result) + } ?: run { + println("Error: Report generation failed") + } + } catch (e: Exception) { + println("Error: ${e.message}") + } } + finalizedBy(extractRequirements) } + + // run ./gradlew :extractRequirements to prepare a folder for the gutachter + project.tasks.register(extractRequirements) { + val sourceRoot = project.rootDir + val targetDir = File(sourceRoot, gutachterFolder) + from(File(sourceRoot, "requirements")) { + include("audit_script.js") + include("audit_style.css") + into("requirements") + } + from(File(sourceRoot, "requirements-report.html")) + into(targetDir) + + doLast { + println("Files copied to ${targetDir.absolutePath}") + } + } + } + + private fun init(project: Project) { + this.project = project + val requirementsFile = project.file(REQUIREMENTS_FILE) + requirementProperties.load(requirementsFile.reader()) + reportGenerator = HtmlReportGenerator(project) } - private fun processSourceDirectory(sourceDirs: Set) { - val allAnnotations = mutableListOf() + private fun loadSpecificationSourceProvider( + repository: SourceSpecificationRepository = SourceSpecificationRepository(), + extractSourceSpecificationUseCase: ExtractSourceSpecificationUseCase = ExtractSourceSpecificationUseCase(), + getSourceSpecificationUseCase: GetSourceSpecificationUseCase = GetSourceSpecificationUseCase(repository) + ): SpecificationSourceProvider = SpecificationSourceProvider( + getSourceSpecificationUseCase = getSourceSpecificationUseCase, + extractSourceSpecificationUseCase = extractSourceSpecificationUseCase + ) + + /** + * Generate the requirements from the source directories + * Create a list of [RequirementData] objects which hold the requirement header and the body + */ + private fun generateRequirements(sourceDirs: Set): List { + val annotations = mutableListOf() + val annotationBodies = mutableListOf() + val annotationHeaders = mutableSetOf() for (sourceDir in sourceDirs) { val files = sourceDir.walk().filter { it.isFile } files.forEach { file -> - val annotationData = findAnnotationsInFile(file) - allAnnotations.addAll(annotationData) + val annotationBody = extractRequirementBody(file) + annotationBody.map { it.requirement }.forEach { requirement -> + annotationHeaders.add(requirement.sanitizeRequirement()) + } + annotationBodies.addAll(annotationBody) } } - generateHtmlReport(allAnnotations) - } - private fun findAnnotationsInFile(file: File): MutableList { - val foundAnnotations = mutableListOf() + // TODO: Check for empty header + annotationHeaders.forEach { requirement -> + val bodies = annotationBodies.filter { it.requirement.sanitizeRequirement() == requirement } + val specification = bodies.firstOrNull()?.specification ?: "" + val specificationSource = SpecificationSource.fromSpec(specification) + annotations.add( + RequirementData( + AnnotationHeader( + specification = specification, + requirement = requirement, + specificationSource = specificationSource + ), + bodies + ) + ) + } + + annotationHeaders.clear() + annotationBodies.clear() + + return annotations.toList() + } + @Suppress("MagicNumber") + private fun extractRequirementBody(file: File): MutableList { + val annotationBodies = mutableListOf() val content = file.readText() val projectRootDir = project?.rootDir?.canonicalPath ?: file.parentFile.canonicalPath val relativePath = file.canonicalPath.removePrefix(projectRootDir) val fileName = relativePath.removePrefix(File.separator) - val matches = ANNOTATION_REGEX.findAll(content) - for (match in matches) { - val annotationText = match.value - val startLine = content.substring(0, match.range.first).count { it == '\n' } + 1 + val requirementMatches = ANNOTATION_REGEX.findAll(content) + for (requirementMatch in requirementMatches) { + val annotationText = requirementMatch.value + val startLine = content.substring(0, requirementMatch.range.first).count { it == '\n' } + 1 + val code = content.split(annotationText)[1].removeAllRequirementAnnotations() val requirements = parseRequirements(annotationText) val specificationValue = parseSpecification(annotationText) val rationaleValue = parseRationale(annotationText) - for (requirement in requirements) { - foundAnnotations.add( - AnnotationData( - fileName, - startLine, - requirement, - specificationValue, - rationaleValue + requirements.forEach { requirement -> + annotationBodies.add( + AnnotationBody( + fileName = fileName, + line = startLine, + requirement = requirement, + specification = specificationValue, + rationale = rationaleValue, + codeBlock = CodeBlock( + code = when { + code.isNotEmpty() && code.isNotBlank() -> { + val requiredCodeLines = parseCodeLines(annotationText, 15) + val codeLines = code.split("\n").take(requiredCodeLines) + codeLines + } + + else -> emptyList() + } + ) ) ) } } - return foundAnnotations + return annotationBodies } private fun parseRequirements(annotationText: String): List { @@ -99,20 +238,7 @@ class TechnicalRequirementsPlugin : Plugin { return requirements } - private fun isSpecification(text: String): Boolean { - // Add any other specification values that should be excluded here - val excludedSpecifications = setOf( - "gemSpec_eRp_FdV", - "BSI-eRp-ePA", - "gemF_Tokenverschlüsselung", - "gemSpec_IDP_Frontend", - "gemSpec_Krypt", - "unused", - "gemF_Biometrie", - "E-Rezept-App-Authentifizierungskonzept.pdf" - ) - return text in excludedSpecifications - } + private fun isSpecification(text: String): Boolean = text in excludedSpecifications private fun parseSpecification(annotationText: String): String { val match = SPEC_REGEX.find(annotationText) @@ -121,121 +247,26 @@ class TechnicalRequirementsPlugin : Plugin { private fun parseRationale(annotationText: String): String { val match = RATIONALE_REGEX.find(annotationText) - println(annotationText) - println(match?.value ?: "_________") return match?.value ?: "" } - private fun extractTextInsideQuotes(input: String): String { - return QUOTES_REGEX.find(input)?.value?.replace("\"", "") ?: "" - } - - companion object { - private val ANNOTATION_REGEX = Regex("""@Requirement\([^)]*\)""", RegexOption.MULTILINE) - val REQUIREMENT_REGEX = Regex("""\s*"([a-zA-Z_][a-zA-Z0-9_#-.]*)"\s*,?\s*""") - private val SPEC_REGEX = Regex("""sourceSpecification\s*=\s*"(.*?)"""") - private val RATIONALE_REGEX = Regex("""rationale\s*=\s*[^)]*""", RegexOption.MULTILINE) - private val QUOTES_REGEX = Regex(""""(.*?)"""") - } - - private fun generateHtmlReport(annotationList: List) { - val specifications = annotationList.map { it.specification }.distinct() - - val htmlBuilder = StringBuilder() - - htmlBuilder.append("") - htmlBuilder.append("") - htmlBuilder.append("") - htmlBuilder.append("") - htmlBuilder.append("Technical Requirements Report") - - htmlBuilder.append("") - htmlBuilder.append("") - - htmlBuilder.append("

Technical Requirements Report

") - - htmlBuilder.append("
    ") - for (specification in specifications) { - htmlBuilder.append("
  • ") - htmlBuilder.append("

    $specification

    ") - htmlBuilder.append("
      ") - val annotationDataList = annotationList.filter { it.specification == specification } - .sortedWith(compareBy { it.requirement }.thenBy { it.extractSuffixNumber() }) - for (data in annotationDataList) { - htmlBuilder.append("
    • ") - htmlBuilder.append( - "

      " + - "${data.requirement}

      " - ) - htmlBuilder.append("
      ${data.toHTML()}
      ") - htmlBuilder.append("
    • ") + @Suppress("MagicNumber") + private fun parseCodeLines(annotationText: String, defaultLines: Int = 15): Int { + val match = CODE_LINES_REGEX.find(annotationText) + return match?.value?.let { value -> + val integerValue = value.split(CODE_LINE).last().trim() + try { + Integer.parseInt(integerValue) + } catch (e: NumberFormatException) { + println("Error: ${e.stackTraceToString()}") + defaultLines } - htmlBuilder.append("
    ") - htmlBuilder.append("
  • ") - } - htmlBuilder.append("
") - - htmlBuilder.append("") - htmlBuilder.append("") - - htmlBuilder.append("") - - htmlBuilder.append("") - - val outputFile = File(project?.rootDir?.path + "/technical_requirements_report.html") - if (!outputFile.exists()) { - outputFile.createNewFile() - } - outputFile.writeText(htmlBuilder.toString()) + } ?: defaultLines } -} -data class AnnotationData( - val fileName: String, - val line: Int, - val requirement: String, - val specification: String, - val rationale: String, - val suffixNumber: Int = 0 // Default value for requirements without a suffix - -) { - - @Suppress("MagicNumber") - fun extractSuffixNumber(): Int { - val suffixIndex = requirement.indexOf('#') - return if (suffixIndex != -1 && suffixIndex < requirement.length - 1) { - requirement.substring(suffixIndex + 1).toIntOrNull() ?: 0 - } else { - 0 - } - } - fun toHTML(): String { - return """ -

Filename

-

$fileName

-

Line

-

$line

-

Description

-

${formatDescription(rationale)}

- """.trimIndent() + private fun extractTextInsideQuotes(input: String): String { + return QUOTES_REGEX.find(input)?.value?.replace("\"", "") ?: "" } - private fun formatDescription(description: String): String { - return description - .replace("rationale =", "") - .replace("\"", "") - .replace("+", " ") - .replace("\n", "
") - .removeSuffix(")") - } + private fun Project.getToken(): String? = findProperty("token") as? String } diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsReadme.md b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsReadme.md new file mode 100644 index 00000000..07eb51a5 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsReadme.md @@ -0,0 +1,47 @@ +# How to run Technical Requirements + +There are three tasks to prepare the technical requirements for the review. + +- downloadBsiSpecs +- generateTechnicalRequirements +- extractRequirements + +## Download Bsi Specs + +- The task is run with the command + +```sh +./gradlew :downloadBsiSpecs -Ptoken=YOUR_PRIVATE_TOKEN +``` + +#### NOTE: If this file is already downloaded, this task is not required to be run again. + +- This token is required to download the specs from the shared repository. Please generate your own + token. +- Once this is downloaded, please check if the file `bsi-requirements.html` is available in + the `requirements` folder. + +## Generate Technical Requirements + +- This task is run with the command + +```sh +./gradlew :generateTechnicalRequirements +``` + +Running this task creates the `requirements-report.html` at the root level. + +## Prepare requirements for the Gutachter + +- This task is run with the command + +```sh +./gradlew :extractRequirements +``` + +#### NOTE: `./gradlew :generateTechnicalRequirements` calls this task once it ends. So this might not be required to call. + +- This copies the `requirements-report.html` and its required css and scripts to a folder + names `gutachter` at root level. This can be presented for review, + +- The files under `gutachter` folder are auto-generated. Please do not modify them. diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationBody.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationBody.kt new file mode 100644 index 00000000..ae8daf11 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationBody.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.model + +import de.gematik.ti.erp.gradleplugins.CODE_LINE +import de.gematik.ti.erp.gradleplugins.RATIONALE + +data class AnnotationBody( + val fileName: String, + val line: Int, + val requirement: String, + val specification: String, + val rationale: String, + val codeBlock: CodeBlock, + val suffixNumber: Int = 0 // Default value for requirements without a suffix + +) { + + fun fileLink() = "https://github.com/gematik/E-Rezept-App-Android/blob/master/$fileName#$line" + + @Suppress("MagicNumber") + fun extractSuffixNumber(): Int { + val suffixIndex = requirement.indexOf('#') + return if (suffixIndex != -1 && suffixIndex < requirement.length - 1) { + requirement.substring(suffixIndex + 1).toIntOrNull() ?: 0 + } else { + 0 + } + } + + fun rationale(): String { + return rationale.split(CODE_LINE) + .first() + .replace(RATIONALE, "") + .replace(CODE_LINE, "") + .replace("\",", "") + .replace("\"", "") + .replace("+", " ") + .replace("\n", "") + .removeSuffix(")") + } +} + +data class CodeBlock(val code: List) diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationHeader.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationHeader.kt new file mode 100644 index 00000000..30530e98 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/AnnotationHeader.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.model + +data class AnnotationHeader( + val specification: String, + val requirement: String, + val specificationSource: SpecificationSource? +) diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/ElementHolder.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/ElementHolder.kt new file mode 100644 index 00000000..14d7f0c1 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/ElementHolder.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.model + +sealed interface ElementHolder + +data class Text(val item: String) : ElementHolder + +data class UnorderedList(val items: List) : ElementHolder + +data class Table(val rows: List) : ElementHolder + +data class Cell(val content: String, val isHeader: Boolean, val rowspan: Int = 1, val colspan: Int = 1) + +data class Row(val cells: List) diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/RequirementData.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/RequirementData.kt new file mode 100644 index 00000000..b87b345f --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/RequirementData.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.model + +data class RequirementData( + val header: AnnotationHeader, + val body: List +) + +fun List.filter(specification: String): List { + return filter { annotation -> annotation.header.specification == specification } + .sortedBy { it.header.requirement } +} + +fun List.toRequirementData(specification: String): List = + map { item -> + val specificationSource = SpecificationSource.fromSpec(specification) + val header = AnnotationHeader( + requirement = item, + specification = specification, + specificationSource = specificationSource + ) + val body = mutableListOf() + RequirementData(header, body) + } diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/SpecificationSource.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/SpecificationSource.kt new file mode 100644 index 00000000..b13fee09 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/model/SpecificationSource.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file: Suppress("ktlint:max-line-length", "MaxLineLength") + +package de.gematik.ti.erp.gradleplugins.model + +/** + * Enum class for AFO specifications + */ +enum class SpecificationSource( + val spec: String, + val url: String, + val isFromGemSpec: Boolean = true +) { + GEM_SPEC_KRYPT( + spec = "gemSpec_Krypt", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_Krypt/latest/index.html" + ), + GEM_SPEC_IDP_FRONTEND( + spec = "gemSpec_IDP_Frontend", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_Frontend/latest/index.html" + ), + GEM_SPEC_ERP_FDV( + spec = "gemSpec_eRp_FdV", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_eRp_FdV/latest/index.html" + ), + GEM_SPEC_IDP_SEK( + spec = "gemSpec_IDP_Sek", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_Sek/latest/index.html" + ), + GEM_SPEC_IDP_DIENSTE( + spec = "gemSpec_IDP_Dienst", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_IDP_Dienst/latest/index.html" + ), + GEM_SPEC_ERP_APOVZD( + spec = "gemSpec_eRp_APOVZD", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_eRp_APOVZD/latest/index.html" + ), + GEM_SPEC_FD_ERP( + spec = "gemSpec_FD_eRp", + url = "https://gemspec.gematik.de/docs/gemSpec/gemSpec_FD_eRp/latest/index.html" + ), + + @Suppress("ktlint:max-line-length", "MaxLineLength") + BSI_ERP_EPA( + spec = "BSI-eRp-ePA", + url = "https://gitlab.prod.ccs.gematik.solutions/api/v4/projects/833/repository/files/security_review%2Frequirements%2F2024.html/raw?ref=main", + isFromGemSpec = false + ); + + companion object { + fun fromSpec(spec: String): SpecificationSource? = values().find { it.spec == spec.trim() } + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/provider/SpecificationSourceProvider.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/provider/SpecificationSourceProvider.kt new file mode 100644 index 00000000..8033b69a --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/provider/SpecificationSourceProvider.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.provider + +import de.gematik.ti.erp.gradleplugins.model.ElementHolder +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import de.gematik.ti.erp.gradleplugins.usecase.ExtractSourceSpecificationUseCase +import de.gematik.ti.erp.gradleplugins.usecase.GetSourceSpecificationUseCase + +class SpecificationSourceProvider( + private val getSourceSpecificationUseCase: GetSourceSpecificationUseCase, + private val extractSourceSpecificationUseCase: ExtractSourceSpecificationUseCase +) { + fun getSourceElements( + source: SpecificationSource, + targetRequirement: String, + isFromGemSpec: Boolean, + onSuccess: (Pair>) -> Unit, + onFailure: () -> Unit + ) { + getSourceSpecificationUseCase.invoke( + source = source, + isFromGemSpec = isFromGemSpec, + targetRequirement = targetRequirement + ) + .fold( + onSuccess = { (html, element) -> + extractSourceSpecificationUseCase + .invoke(sourceElement = element) + .let { onSuccess(html to it) } + }, + onFailure = { e -> + println(" $targetRequirement: Exception in getting source element ${e.stackTraceToString()}") + onFailure() + } + ) + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/ReportGenerator.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/ReportGenerator.kt new file mode 100644 index 00000000..9bff84dd --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/ReportGenerator.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.reports + +import de.gematik.ti.erp.gradleplugins.model.RequirementData +import de.gematik.ti.erp.gradleplugins.provider.SpecificationSourceProvider +import org.gradle.api.Project +import java.util.Properties + +@Suppress("UnusedPrivateProperty", "UnusedPrivateMember") +abstract class ReportGenerator(private val project: Project?) { + abstract fun generate( + project: Project, + requirements: List, + properties: Properties? = null, + specificationSourceProvider: SpecificationSourceProvider + ): String +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportComponents.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportComponents.kt new file mode 100644 index 00000000..0e0332d9 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportComponents.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.reports.html + +import de.gematik.ti.erp.gradleplugins.model.AnnotationBody +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import kotlinx.html.BODY +import kotlinx.html.ButtonType +import kotlinx.html.FlowContent +import kotlinx.html.FlowOrHeadingContent +import kotlinx.html.FlowOrPhrasingContent +import kotlinx.html.HTML +import kotlinx.html.a +import kotlinx.html.b +import kotlinx.html.br +import kotlinx.html.button +import kotlinx.html.code +import kotlinx.html.div +import kotlinx.html.h2 +import kotlinx.html.h3 +import kotlinx.html.h4 +import kotlinx.html.h5 +import kotlinx.html.head +import kotlinx.html.hr +import kotlinx.html.id +import kotlinx.html.li +import kotlinx.html.link +import kotlinx.html.meta +import kotlinx.html.onClick +import kotlinx.html.p +import kotlinx.html.pre +import kotlinx.html.span +import kotlinx.html.sub +import kotlinx.html.title +import kotlinx.html.ul + +/** + * HTML components for the technical requirements report + * NOTE: The classes and ids used in the components + * all are related to requirements/audit_style.css and + * requirements/audit_script.js. Please make sure of those things + * before changing the classes and ids. + */ + +fun HTML.pageHeader() { + head { + meta { charset = "utf-8" } + meta { name = "viewport"; content = "width=device-width, initial-scale=1" } + title { +"Requirements" } + // stylesheets + link(href = "requirements/audit_style.css", rel = "stylesheet") + link( + href = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css", + rel = "stylesheet" + ) + } +} + +fun BODY.pageTitle() { + h3("centered-text") { +"TECHNICAL REQUIREMENTS" } +} + +fun BODY.floatingActionScrollToTopButton() { + button(classes = "fab", type = ButtonType.button) { + id = "scrollToTopBtn" + onClick = "scrollToTop()" + +"↑" + } +} + +fun BODY.specificationsTableOfContents(specifications: Set) { + div("toc-box") { + p { b { +"Specifications" } } + ul { + specifications.forEach { specification -> + li { + a("#$specification") { +specification } + } + } + } + } +} + +fun BODY.requirementsTableOfContents(requirements: List) { + p { b { +"Requirements" } } + div("toc-grid") { + requirements.forEach { requirement -> + div("toc-item") { + a("#$requirement") { +requirement } + } + } + } +} + +fun BODY.specification(value: String) { + h2 { + id = value + b { +value } + } +} + +fun BODY.specificationSourceLink(value: SpecificationSource) { + p { a(href = value.url) { +value.url } } +} + +fun BODY.requirementHeader(value: String) { + h4 { + id = value + +value + } +} + +fun BODY.notesTitle() { + p { b { +"Notes and Code" } } +} + +fun BODY.notesBox(value: String) { + p { +value } +} + +fun FlowOrPhrasingContent.lineBreak() { + br { } +} + +fun BODY.horizontalLine() { + hr { } +} + +fun FlowOrPhrasingContent.fileLink(value: String) { + sub { + a( + href = value, + classes = "file-link" + ) { +value } + } +} + +fun FlowContent.codeBlock(body: AnnotationBody) { + val codeBlockId = "codeBlock-${body.hashCode()}" + + div(classes = "toggle-row") { + attributes["data-target"] = codeBlockId + span(classes = "code-block-label") { fileLink(body) } + span(classes = "arrow") { +"▼" } + } + + pre(classes = "code-box") { + id = codeBlockId + code(classes = "language-kt") { + body.codeBlock.code.forEach { + val codeLine = it.trim() + if (codeLine.isNotEmpty() && codeLine.isNotBlank()) { + p { +it } + } + } + } + } +} + +fun FlowOrHeadingContent.subTitle(body: AnnotationBody) { + h5 { + span(classes = "box") { + +"${body.requirement} - ${body.extractSuffixNumber()}" + } + } +} + +fun FlowContent.description(value: String) { + p { +value } +} + +private fun FlowOrPhrasingContent.fileLink(body: AnnotationBody) { + sub { + a( + href = body.fileLink(), + classes = "file-link" + ) { +body.fileName } + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportGenerator.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportGenerator.kt new file mode 100644 index 00000000..3cc78ca6 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/reports/html/HtmlReportGenerator.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.reports.html + +import de.gematik.ti.erp.gradleplugins.NO_LINK +import de.gematik.ti.erp.gradleplugins.model.ElementHolder +import de.gematik.ti.erp.gradleplugins.model.RequirementData +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import de.gematik.ti.erp.gradleplugins.model.Table +import de.gematik.ti.erp.gradleplugins.model.Text +import de.gematik.ti.erp.gradleplugins.model.UnorderedList +import de.gematik.ti.erp.gradleplugins.model.filter +import de.gematik.ti.erp.gradleplugins.provider.SpecificationSourceProvider +import de.gematik.ti.erp.gradleplugins.reports.ReportGenerator +import kotlinx.html.b +import kotlinx.html.body +import kotlinx.html.div +import kotlinx.html.dom.createHTMLDocument +import kotlinx.html.dom.serialize +import kotlinx.html.html +import kotlinx.html.li +import kotlinx.html.p +import kotlinx.html.script +import kotlinx.html.table +import kotlinx.html.td +import kotlinx.html.th +import kotlinx.html.tr +import kotlinx.html.ul +import org.gradle.api.Project +import java.util.Properties + +class HtmlReportGenerator(project: Project?) : ReportGenerator(project) { + + private var requirementProperties = Properties() + + @Suppress("NestedBlockDepth", "CyclomaticComplexMethod") + override fun generate( + project: Project, + requirements: List, + properties: Properties?, + specificationSourceProvider: SpecificationSourceProvider + ): String { + if (properties != null) { + requirementProperties = properties + } else { + throw IllegalArgumentException("Properties file must not be null") + } + + val specifications = requirements.map { requirement -> requirement.header.specification }.distinct().toSet() + + val html = createHTMLDocument().html { + pageHeader() + body { + script(src = "requirements/audit_script.js") {} + script(src = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js") {} + script(src = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-kotlin.min.js") {} + pageTitle() + lineBreak() + specificationsTableOfContents(specifications) + + floatingActionScrollToTopButton() + + for (specification in specifications) { + val specificationSource = SpecificationSource.fromSpec(specification) + specification(value = specification) + // Do not place the link if it is a BSI specification + if (specification != SpecificationSource.BSI_ERP_EPA.spec) { + specificationSource?.let(::specificationSourceLink) + } + val codeRequirements = requirements.filter(specification.trim()).toSet() + val codeRequirementHeaders = codeRequirements.map { it.header.requirement.trim() }.toSet() + + // load the items based on the specification block we are in + val requirementSubTitles = requirementProperties.stringPropertyNames() + .filter { it.startsWith(specification.trim()) } + .map { it.split("$specification.")[1] } + + // combine the requirements from the code and the properties file and group them by prefix + // and sort them by number + val combinedRequirementHeaders = codeRequirementHeaders + .union(requirementSubTitles) + .groupBy { extractPrefix(it) } + .toSortedMap() + .flatMap { entry -> entry.value.sortedWith(compareBy { extractNumber(it) }) } + + requirementsTableOfContents(combinedRequirementHeaders) + + combinedRequirementHeaders.forEach { filteredRequirementHeader -> + + // get info from the properties file + val notes = requirementProperties.getProperty("$specification.$filteredRequirementHeader") + requirementHeader(filteredRequirementHeader) + + // download the specification source + specificationSource?.let { + specificationSourceProvider.getSourceElements( + source = specificationSource, + targetRequirement = filteredRequirementHeader, + isFromGemSpec = specificationSource.isFromGemSpec, + onSuccess = { (sourceHtml: String, elements: List) -> + // BSIs do not have a source link + if (sourceHtml != NO_LINK) { + fileLink(sourceHtml) + } + // lineBreak() + div("box") { + elements.forEachIndexed { index, element -> + if (element is Text && index == 0) { + p { b { +element.item } } + } else { + if (element is Text) { + p { +element.item } + } + if (element is UnorderedList) { + ul { + element.items.forEach { + li { + p { +it } + } + } + } + } + if (element is Table) { + table("centered-text") { + element.rows.forEach { row -> + tr { + row.cells.forEach { cell -> + if (cell.isHeader) { + th { + +cell.content + rowSpan = cell.rowspan.toString() + colSpan = cell.colspan.toString() + } + } else { + td { + +cell.content + rowSpan = cell.rowspan.toString() + colSpan = cell.colspan.toString() + } + } + } + } + } + } + } + } + } + lineBreak() + } + }, + onFailure = { + println("Probably wrong in document: $filteredRequirementHeader") + } + ) + } + notesTitle() + if (notes != null) { + notesBox(notes) + } + + if (codeRequirements.isEmpty()) { + horizontalLine() + } + // print the code from the project + codeRequirements + .filter { it.header.requirement == filteredRequirementHeader } + .map { filteredRequirement -> + div(classes = "visible") { + filteredRequirement.body + .sortedBy { it.requirement } + .sortedBy { it.extractSuffixNumber() } + .forEach { requirementBody -> + requirementBody.apply { + subTitle(this) + description(this.rationale()) + if (codeBlock.code.isNotEmpty()) { + // fileLink(this) + codeBlock(this) + lineBreak() + } + } + } + } + } + if (codeRequirements.isNotEmpty()) { + horizontalLine() + } + } + } + } + } + + val result = html.serialize(prettyPrint = true) + return result + } + + private fun extractPrefix(value: String): String { + // Extract prefix (e.g., "O.Auth", "O.Arch") + return value.split("_").firstOrNull() ?: value + } + + private fun extractNumber(value: String): Int { + // Find the first occurrence of a numeric segment in the string + val regex = "\\d+".toRegex() + val match = regex.find(value) + return match?.value?.toInt() ?: Int.MAX_VALUE + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/repository/SourceSpecificationRepository.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/repository/SourceSpecificationRepository.kt new file mode 100644 index 00000000..355e235e --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/repository/SourceSpecificationRepository.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.repository + +import de.gematik.ti.erp.gradleplugins.BSI_REQUIREMENTS_PATH +import de.gematik.ti.erp.gradleplugins.NO_LINK +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.io.File + +private const val TARGET_ELEMENT = "target-element" + +class SourceSpecificationRepository { + // download the source specification from gemspec or gitlab + @Suppress("NestedBlockDepth") + fun download( + source: SpecificationSource, + isFromGemSpec: Boolean, + targetRequirement: String + ): Result> = try { + val document: Document? + val updatedHtml: String + if (isFromGemSpec) { + updatedHtml = source.url + document = Jsoup.connect(source.url).get() + // download the latest version of the source + /* if (document.getElementById("versionDropDownButton") != null && !source.useLatestOnly) { + val versionDropDownButton = document.getElementById("versionDropDownButton") + val version = versionDropDownButton?.text() + val html = source.url.replace("latest/index.html", "${source.spec}_V$version.html") + updatedHtml = html + document = Jsoup.connect(html).get() + } */ + val elements = document.getElementsByClass(TARGET_ELEMENT) + val targetElement = elements.find { it.id() == targetRequirement } + targetElement?.let { Result.success(updatedHtml to it) } + ?: run { Result.failure(Exception("Element not found")) } + } else { + val htmlFile = File(BSI_REQUIREMENTS_PATH) + Jsoup.parse(htmlFile).let { bsiDocument -> + val elements = bsiDocument.getElementsByClass(TARGET_ELEMENT) + // O.Source_5 as OSource_5 in the html file + val updatedTargetRequirement = targetRequirement.replace(".", "") + val targetElement = elements.find { it.id() == updatedTargetRequirement } + targetElement?.let { Result.success(NO_LINK to it) } + ?: run { Result.failure(Exception("$TARGET_ELEMENT not found in local bsi-spec html file")) } + } + } + } catch (e: Exception) { + println("Error for $source for requirement:$targetRequirement ") + println("Error message: ${e.message}") + Result.failure(e) + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/ExtractSourceSpecificationUseCase.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/ExtractSourceSpecificationUseCase.kt new file mode 100644 index 00000000..7c68858c --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/ExtractSourceSpecificationUseCase.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.usecase + +import de.gematik.ti.erp.gradleplugins.model.Cell +import de.gematik.ti.erp.gradleplugins.model.ElementHolder +import de.gematik.ti.erp.gradleplugins.model.Row +import de.gematik.ti.erp.gradleplugins.model.Table +import de.gematik.ti.erp.gradleplugins.model.Text +import de.gematik.ti.erp.gradleplugins.model.UnorderedList +import org.jsoup.nodes.Element + +class ExtractSourceSpecificationUseCase { + + operator fun invoke( + sourceElement: Element + ): List { + val elements = mutableListOf() + + // extract paragraphs + sourceElement.addParagraphs { paragraph -> + elements.add(paragraph) + } + + // extract unordered lists + sourceElement.addLists { unorderedList -> + elements.add(unorderedList) + } + + // extract tables + sourceElement.addTables { table -> + elements.add(table) + } + + return elements + } +} + +private fun Element.addTables( + action: (Table) -> Unit +) { + val tableElements = this.select("table") + + tableElements.forEach { tableElement -> + val rows = mutableListOf() + tableElement.select("tr").map { rowElement -> + val cells = mutableListOf() + rowElement.children().forEach { cellElement -> + val isHeader = cellElement.tagName() == "th" + val rowspan = cellElement.attr("rowspan").ifEmpty { "1" }.toInt() + val colspan = cellElement.attr("colspan").ifEmpty { "1" }.toInt() + cells.add(Cell(cellElement.text(), isHeader, rowspan, colspan)) + } + rows.add(Row(cells)) + } + val table = Table(rows) + action(table) + } +} + +private fun Element.addLists( + action: (UnorderedList) -> Unit +) { + val listElements = this.select("ul") + + listElements.forEach { listElement -> + val items = mutableListOf() + listElement.select("li").map { itemElement -> + items.add(itemElement.text()) + } + val unorderedList = UnorderedList(items) + action(unorderedList) + } +} + +private fun Element.addParagraphs( + action: (Text) -> Unit +) { + val paragraphElements = this.select("p") + + paragraphElements.forEach { paragraphElement -> + action(Text(paragraphElement.text())) + } +} diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/GetSourceSpecificationUseCase.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/GetSourceSpecificationUseCase.kt new file mode 100644 index 00000000..17ea7178 --- /dev/null +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/usecase/GetSourceSpecificationUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.gradleplugins.usecase + +import de.gematik.ti.erp.gradleplugins.model.SpecificationSource +import de.gematik.ti.erp.gradleplugins.repository.SourceSpecificationRepository +import org.jsoup.nodes.Element + +class GetSourceSpecificationUseCase( + private val repository: SourceSpecificationRepository +) { + operator fun invoke( + source: SpecificationSource, + isFromGemSpec: Boolean, + targetRequirement: String + ): Result> = repository.download( + source = source, + isFromGemSpec = isFromGemSpec, + targetRequirement = targetRequirement + ) +} diff --git a/rules/build.gradle.kts b/rules/build.gradle.kts index 869f2ee4..c84662d0 100644 --- a/rules/build.gradle.kts +++ b/rules/build.gradle.kts @@ -12,19 +12,23 @@ repositories { version = 1.0 group = "de.gematik.ti.erp.app" -tasks.withType() { +tasks.withType { kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } +tasks.withType { + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") +} + tasks.test { useJUnitPlatform() } dependencies { implementation(kotlin("stdlib")) - implementation("com.pinterest.ktlint:ktlint-core:0.45.2") + implementation(libs.quality.ktlint.core) testImplementation(kotlin("test")) - testImplementation("junit:junit:4.13.2") - testImplementation("com.pinterest.ktlint:ktlint-test:0.45.2") + testImplementation(libs.test.junit) + testImplementation(libs.quality.ktlint.test) } diff --git a/rules/settings.gradle.kts b/rules/settings.gradle.kts index 4f3c6276..2a298eb7 100644 --- a/rules/settings.gradle.kts +++ b/rules/settings.gradle.kts @@ -5,3 +5,10 @@ pluginManagement { mavenCentral() } } +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/rules/src/main/kotlin/de/gematik/ti/erp/CustomRuleSetProvider.kt b/rules/src/main/kotlin/de/gematik/ti/erp/CustomRuleSetProvider.kt index eca100a6..3fffcdb5 100644 --- a/rules/src/main/kotlin/de/gematik/ti/erp/CustomRuleSetProvider.kt +++ b/rules/src/main/kotlin/de/gematik/ti/erp/CustomRuleSetProvider.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp diff --git a/rules/src/main/kotlin/de/gematik/ti/erp/LicenceRule.kt b/rules/src/main/kotlin/de/gematik/ti/erp/LicenceRule.kt index 5e5263a4..41d79172 100644 --- a/rules/src/main/kotlin/de/gematik/ti/erp/LicenceRule.kt +++ b/rules/src/main/kotlin/de/gematik/ti/erp/LicenceRule.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp @@ -29,50 +29,114 @@ import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -val licenceHeader = """ +val licencePlaceholderHeader = + """ /* * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} */ -""".trimIndent() + """.trimIndent() +val oldLicenceHeader = + """ + /* + * Copyright 2023, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + """.trimIndent() + +val licenceHeader = + """ + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + """.trimIndent() + +@Suppress("NestedBlockDepth") class LicenceRule : Rule("licence-header") { override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - if (node.elementType == KtStubElementTypes.FILE) { - if ((node.psi as KtFile).isScript()) { - return + if (node.elementType == KtStubElementTypes.FILE && (node.psi as? KtFile)?.isScript() == false) { + node.children().forEach { child -> + if (child.elementType == KtTokens.BLOCK_COMMENT) { + when { + child.isLicencePlaceholderHeader() -> { + emit(child.startOffset, "Licence header placeholder found", true) + if (autoCorrect) removeNodeWithWhitespace(node, child) + } + child.isOldLicenceHeader() -> { + emit(child.startOffset, "Deprecated licence header found", true) + if (autoCorrect) removeNodeWithWhitespace(node, child) + } + child.isMisplacedLicenceHeader() -> { + emit(child.startOffset, "Misplaced licence header found", true) + if (autoCorrect) removeNodeWithWhitespace(node, child) + } + } + } } - val commentChild = node.findChildByType(KtTokens.BLOCK_COMMENT) + if (!node.firstChildNode.isLicenceHeader()) { + emit(node.startOffset, "Licence header missing", true) + if (autoCorrect) addLicenceHeader(node) + } + } + } - val licenceHeaderFound = - commentChild != null && commentChild.lineNumber() == 1 && - commentChild.text.trim() == licenceHeader.trim() + private fun ASTNode.isLicenceHeader() = + this.elementType == KtTokens.BLOCK_COMMENT && this.text.trim() == licenceHeader.trim() && this.lineNumber() == 1 - if (!licenceHeaderFound) { - emit(node.startOffset, "Licence header missing", true) + private fun ASTNode.isLicencePlaceholderHeader() = + this.text.trim() == licencePlaceholderHeader.trim() - if (autoCorrect) { - node.children().forEach { child -> - // remove all duplicates or misplaced licence headers - if (child.elementType == KtTokens.BLOCK_COMMENT && child.text == licenceHeader) { - val firstNode = child.treePrev?.takeIf { it.isWhiteSpaceWithNewline() } ?: child - val lastNode = child.treeNext?.takeIf { it.isWhiteSpaceWithNewline() } + private fun ASTNode.isOldLicenceHeader() = + this.text.trim() == oldLicenceHeader.trim() - node.removeRange(firstNode, lastNode) - } - } - val licenceNode = PsiCommentImpl(KtTokens.BLOCK_COMMENT, licenceHeader) - node.addChild( - licenceNode, - node.firstChildNode - ) - licenceNode.upsertWhitespaceAfterMe("\n\n") - } - } + private fun ASTNode.isMisplacedLicenceHeader() = + this.text.trim() == licenceHeader.trim() && this.lineNumber() != 1 + + private fun removeNodeWithWhitespace(node: ASTNode, child: ASTNode) { + val firstNode = child.treePrev?.takeIf { it.isWhiteSpaceWithNewline() } + val lastNode = child.treeNext?.takeIf { it.isWhiteSpaceWithNewline() } + if (firstNode != null && lastNode != null) { + node.removeRange(firstNode, lastNode) + } else { + node.removeChild(child) } } + + private fun addLicenceHeader(node: ASTNode) { + val licenceNode = PsiCommentImpl(KtTokens.BLOCK_COMMENT, licenceHeader) + node.addChild(licenceNode, node.firstChildNode) + licenceNode.upsertWhitespaceAfterMe("\n\n") + } } diff --git a/rules/src/test/kotlin/de/gematik/ti/erp/LicenceRuleTest.kt b/rules/src/test/kotlin/de/gematik/ti/erp/LicenceRuleTest.kt index d1476645..9f3f3dab 100644 --- a/rules/src/test/kotlin/de/gematik/ti/erp/LicenceRuleTest.kt +++ b/rules/src/test/kotlin/de/gematik/ti/erp/LicenceRuleTest.kt @@ -1,19 +1,19 @@ /* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package de.gematik.ti.erp @@ -21,98 +21,196 @@ package de.gematik.ti.erp import com.pinterest.ktlint.core.LintError import com.pinterest.ktlint.test.format import com.pinterest.ktlint.test.lint -import kotlin.test.assertEquals import kotlin.test.Test +import kotlin.test.assertEquals class LicenceRuleTest { @Test fun `lint missing licence header`() { - val lintErrors = LicenceRule().lint( - """ - /* */ - - package a.b.c - - import d.e.f - """.trimIndent() - ) + val lintErrors = + LicenceRule().lint( + """ + /* */ + + package a.b.c + + import d.e.f + """.trimIndent() + ) - val expected = LintError( - 1, - 1, - "licence-header", - "Licence header missing" - ) + val expected = + LintError( + 1, + 1, + "licence-header", + "Licence header missing" + ) assertEquals(listOf(expected), lintErrors) } @Test fun `lint found licence header`() { - val lintErrors = LicenceRule().lint( - """ - /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} - */ - - package a.b.c - - import d.e.f - """.trimIndent() - ) + val lintErrors = + LicenceRule().lint( + """ + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + + package a.b.c + + import d.e.f + """.trimIndent() + ) assertEquals(emptyList(), lintErrors) } @Test fun `lint found licence header in wrong position`() { - val lintErrors = LicenceRule().lint( - """ - /* */ - - /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} - */ - - package a.b.c - - import d.e.f - """.trimIndent() - ) + val lintErrors = + LicenceRule().lint( + """ + /* */ + + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + + package a.b.c + + import d.e.f + """.trimIndent() + ) - val expected = LintError( - 1, - 1, - "licence-header", - "Licence header missing" + val expected = listOf( + LintError( + 1, + 1, + "licence-header", + "Licence header missing" + ), + LintError( + 3, + 1, + "licence-header", + "Misplaced licence header found" + ) ) - - assertEquals(listOf(expected), lintErrors) + assertEquals(expected, lintErrors) } @Test fun `format licence header - comment first`() { - val given = LicenceRule().format( + val given = + LicenceRule().format( + """ + /* Some comment */ + + package a.b.c + + import d.e.f + """.trimIndent() + ) + + val expected = """ + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + /* Some comment */ package a.b.c import d.e.f """.trimIndent() - ) - val expected = """ + assertEquals(expected, given) + } + + @Test + fun `format licence header - old placeholder`() { + val given = + LicenceRule().format( + """ + /* + * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} + */ + + package a.b.c + + import d.e.f + """.trimIndent() + ) + + val expected = + """ /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ - - /* Some comment */ - + package a.b.c import d.e.f - """.trimIndent() + """.trimIndent() assertEquals(expected, given) } @@ -129,9 +227,24 @@ class LicenceRuleTest { """.trimIndent() ) - val expected = """ + val expected = + """ /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package a.b.c @@ -139,62 +252,122 @@ class LicenceRuleTest { import d.e.f import d.e.f import d.e.f - """.trimIndent() + """.trimIndent() assertEquals(expected, given) } @Test fun `format licence header - wrong position`() { - val given = LicenceRule().format( - """ - package a.b.c - - /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} - */ - - import d.e.f - """.trimIndent() - ) + val given = + LicenceRule().format( + """ + package a.b.c + + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + + import d.e.f + """.trimIndent() + ) - val expected = """ + val expected = + """ /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. */ package a.b.c import d.e.f - """.trimIndent() + """.trimIndent() assertEquals(expected, given) } @Test fun `format licence header - script - expect same output`() { - val given = LicenceRule().format( + val given = + LicenceRule().format( + """ + /* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + + import a.b.c + + abc {} + """.trimIndent(), + script = true + ) + + val expected = """ - import a.b.c - /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} - */ - - abc {} - """.trimIndent(), - script = true - ) - - val expected = """ + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + import a.b.c - - /* - * ${'$'}{GEMATIK_COPYRIGHT_STATEMENT} - */ - + abc {} - """.trimIndent() + """.trimIndent() assertEquals(expected, given) } diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/scripts/build.gradle.kts b/scripts/build.gradle.kts new file mode 100644 index 00000000..5b9ef02f --- /dev/null +++ b/scripts/build.gradle.kts @@ -0,0 +1,27 @@ + +plugins { + `kotlin-dsl` + `kotlin-dsl-precompiled-script-plugins` + alias(libs.plugins.detekt) +} + +repositories { + maven("https://oss.sonatype.org/content/repositories/snapshots/") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation(libs.android.gradle.plugin) + implementation(libs.kotlin.gradle.plugin) + implementation(libs.buildkonfig.gradle.plugin) + implementation(libs.secrets.gradle.plugin) + implementation(libs.dependency.check.gradle) + implementation(libs.gradle.license.plugin) + implementation(libs.database.realm.plugin) + implementation(libs.compose.plugin) + implementation(libs.kotlin.serilization.plugin) + implementation(libs.quality.detekt) +} diff --git a/scripts/settings.gradle.kts b/scripts/settings.gradle.kts new file mode 100644 index 00000000..b5a0fabf --- /dev/null +++ b/scripts/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/scripts/src/main/kotlin/base-android-application.gradle.kts b/scripts/src/main/kotlin/base-android-application.gradle.kts new file mode 100644 index 00000000..760ba6ef --- /dev/null +++ b/scripts/src/main/kotlin/base-android-application.gradle.kts @@ -0,0 +1,164 @@ +@file:Suppress("UnusedPrivateProperty", "UnstableApiUsage") + +import com.android.build.api.dsl.ManagedVirtualDevice +import extensions.BuildNames.pixel5Api30 +import extensions.BuildNames.pixel8Api34 +import extensions.BuildNames.versionCatalogLibrary +import extensions.Versions.BUILD_TOOLS_VERSION +import extensions.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET +import extensions.Versions.JavaVersion.PROJECT_JAVA_VERSION +import extensions.Versions.KOTLIN_COMPILER_EXTENSION_VERSION +import extensions.Versions.SdkVersions.COMPILE_SDK_VERSION +import extensions.Versions.SdkVersions.MIN_SDK_VERSION +import extensions.Versions.SdkVersions.TARGET_SDK_VERSION +import extensions.androidTestExtension +import extensions.androidxAppBundle +import extensions.checks +import extensions.composeAppBundle +import extensions.databaseBundle +import extensions.datetimeBundle +import extensions.desugarLibrary +import extensions.diComposeLibrary +import extensions.imageBundle +import extensions.junitExtension +import extensions.lifecycleBundle +import extensions.napierLibrary +import extensions.navigationComposeLibrary +import extensions.networkBundle +import extensions.pdfboxBundle +import extensions.processPhoenixBundle +import extensions.serializationBundle +import extensions.testExtension +import extensions.trackingBundle + +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-parcelize") + kotlin("plugin.serialization") + id("io.realm.kotlin") + id("quality-detekt") + id("org.jetbrains.compose") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") +} + +afterEvaluate { + val taskRegEx = """assemble(Google|Huawei)(PuExternalDebug|PuExternalRelease)""".toRegex() + tasks.forEach { task -> + taskRegEx.matchEntire(task.name)?.let { + val (_, version, flavor) = it.groupValues + task.dependsOn(tasks.getByName("license${version}${flavor}Report")) + } + } +} + +val libs: VersionCatalog = extensions.getByType().named(versionCatalogLibrary) + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +android { + buildToolsVersion = BUILD_TOOLS_VERSION + composeOptions.kotlinCompilerExtensionVersion = KOTLIN_COMPILER_EXTENSION_VERSION + compileSdk = COMPILE_SDK_VERSION + defaultConfig { + minSdk = MIN_SDK_VERSION + targetSdk = TARGET_SDK_VERSION + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + // todo: check later if we need this + // testInstrumentationRunnerArguments += "clearPackageData" to "true" + // testInstrumentationRunnerArguments += "useTestStorageService" to "true" + } + dependencyCheck { + checks(project.configurations) + } + kotlinOptions { + jvmTarget = KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + } + androidResources { + generateLocaleConfig = true + noCompress.addAll(listOf("srt", "csv", "json")) + } + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = PROJECT_JAVA_VERSION + targetCompatibility = PROJECT_JAVA_VERSION + } + testOptions { + this.emulatorControl + execution = "ANDROIDX_TEST_ORCHESTRATOR" + animationsDisabled = true + emulatorSnapshots.compressSnapshots = true + managedDevices.devices { + maybeCreate(pixel5Api30.name).apply { + device = pixel5Api30.device + apiLevel = pixel5Api30.apiLevel + systemImageSource = pixel5Api30.systemImageSource + } + maybeCreate(pixel8Api34.name).apply { + device = pixel8Api34.device + apiLevel = pixel8Api34.apiLevel + systemImageSource = pixel8Api34.systemImageSource + } + } + } + // for JNA and JNA-platform + // for byte-buddy + packaging { + resources { + excludes += "META-INF/**" + // for JNA and JNA-platform + excludes += "META-INF/AL2.0" + excludes += "META-INF/LGPL2.1" + // for byte-buddy + excludes += "META-INF/licenses/ASM" + pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" + pickFirsts += "win32-x86/attach_hotspot_windows.dll" + } + } + + buildFeatures { + buildConfig = true + compose = true + } +} + +dependencies { + implementation(libs.pdfboxBundle) { + exclude(group = "org.bouncycastle") + } + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + implementation(libs.datetimeBundle) + debugImplementation(libs.processPhoenixBundle) + implementation(libs.androidxAppBundle) + implementation(libs.lifecycleBundle) + implementation(libs.trackingBundle) + implementation(libs.composeAppBundle) + compileOnly(libs.databaseBundle) + implementation(libs.networkBundle) + implementation(libs.serializationBundle) + compileOnly(libs.diComposeLibrary) + implementation(libs.napierLibrary) + coreLibraryDesugaring(libs.desugarLibrary) + implementation(libs.navigationComposeLibrary) + implementation(libs.imageBundle) + + androidTestExtension(libs) + testExtension(libs) + junitExtension(libs) +} + +secrets { + defaultPropertiesFileName = when { + project.rootProject.file("ci-overrides.properties").exists() -> "ci-overrides.properties" + else -> "gradle.properties" + } +} diff --git a/scripts/src/main/kotlin/base-android-library.gradle.kts b/scripts/src/main/kotlin/base-android-library.gradle.kts new file mode 100644 index 00000000..d64481ec --- /dev/null +++ b/scripts/src/main/kotlin/base-android-library.gradle.kts @@ -0,0 +1,194 @@ +@file:Suppress("UnstableApiUsage") + +import extensions.BuildNames +import extensions.BuildNames.minifiedDebug +import extensions.BuildNames.versionCatalogLibrary +import extensions.Versions.BUILD_TOOLS_VERSION +import extensions.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET +import extensions.Versions.JavaVersion.PROJECT_JAVA_VERSION +import extensions.Versions.KOTLIN_COMPILER_EXTENSION_VERSION +import extensions.Versions.SdkVersions.COMPILE_SDK_VERSION +import extensions.Versions.SdkVersions.MIN_SDK_VERSION +import extensions.accompanistBundle +import extensions.androidTestExtension +import extensions.androidxBundle +import extensions.animationBundle +import extensions.cameraBundle +import extensions.checks +import extensions.composeBundle +import extensions.coroutinesBundle +import extensions.cryptoBundle +import extensions.databaseBundle +import extensions.datamatrixBundle +import extensions.datetimeBundle +import extensions.diBundle +import extensions.excludeList +import extensions.imageBundle +import extensions.junitExtension +import extensions.lifecycleBundle +import extensions.mapsBundle +import extensions.materialLibrary +import extensions.napierLibrary +import extensions.networkBundle +import extensions.othersBundle +import extensions.pdfboxBundle +import extensions.playBundle +import extensions.processPhoenixBundle +import extensions.serializationBundle +import extensions.testExtension +import extensions.trackingBundle + +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-parcelize") + kotlin("plugin.serialization") + id("io.realm.kotlin") + id("quality-detekt") + id("org.jetbrains.compose") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("jacoco") +} + +val libs: VersionCatalog = + extensions.getByType().named(versionCatalogLibrary) + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +/*configurations.all { + resolutionStrategy { + // Forcing this lib to be of this version for all modules since later version needs CompileSdk = 34 + force("androidx.emoji2:emoji2:1.3.0") + } +}*/ + +android { + buildToolsVersion = BUILD_TOOLS_VERSION + composeOptions.kotlinCompilerExtensionVersion = KOTLIN_COMPILER_EXTENSION_VERSION + compileSdk = COMPILE_SDK_VERSION + defaultConfig { + minSdk = MIN_SDK_VERSION + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + dependencyCheck { + checks(project.configurations) + } + kotlinOptions { + jvmTarget = KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + } + compileOptions { + sourceCompatibility = PROJECT_JAVA_VERSION + targetCompatibility = PROJECT_JAVA_VERSION + } + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + unitTests.isIncludeAndroidResources = true + } + // for JNA and JNA-platform + // for byte-buddy + packaging { + resources { + excludes += "META-INF/**" + // for JNA and JNA-platform + excludes += "META-INF/AL2.0" + excludes += "META-INF/LGPL2.1" + // for byte-buddy + excludes += "META-INF/licenses/ASM" + pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" + pickFirsts += "win32-x86/attach_hotspot_windows.dll" + } + } + + buildTypes { + val debug by getting { + isJniDebuggable = true + enableUnitTestCoverage = true + enableAndroidTestCoverage = true + } + create(minifiedDebug) { + initWith(debug) + } + } + buildFeatures { + buildConfig = true + compose = true + } + androidResources { + noCompress.addAll(listOf("srt", "csv", "json")) + } +} + +jacoco { + toolVersion = BuildNames.jacocoToolsVersion +} + +tasks.register("jacocoTestReport") { + dependsOn("testDebugUnitTest") + + reports { + xml.required.set(true) + html.required.set(true) + } + + val mainSrc = files("src/main/java", "src/main/kotlin") + val debugTree = fileTree("${layout.buildDirectory}/intermediates/javac/debug/classes") + val kotlinDebugTree = fileTree("${layout.buildDirectory}/tmp/kotlin-classes/debug") + val execFiles = fileTree(layout.buildDirectory).include("**/*.exec") + + sourceDirectories.setFrom(mainSrc) + classDirectories.setFrom(files(debugTree, kotlinDebugTree).asFileTree.matching { + excludeList() + }) + executionData.setFrom(execFiles) +} + +dependencies { + implementation(libs.pdfboxBundle) { + exclude(group = "org.bouncycastle") + } + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + implementation(libs.materialLibrary) + implementation(libs.datamatrixBundle) + implementation(libs.coroutinesBundle) + implementation(libs.datetimeBundle) + implementation(libs.accompanistBundle) + implementation(libs.othersBundle) + debugImplementation(libs.processPhoenixBundle) + implementation(libs.androidxBundle) + implementation(libs.lifecycleBundle) + implementation(libs.composeBundle) + implementation(libs.cameraBundle) + compileOnly(libs.databaseBundle) + implementation(libs.diBundle) + implementation(libs.imageBundle) + implementation(libs.cryptoBundle) + implementation(libs.mapsBundle) + implementation(libs.networkBundle) + implementation(libs.animationBundle) + implementation(libs.napierLibrary) + implementation(libs.playBundle) + implementation(libs.serializationBundle) + implementation(libs.trackingBundle) + + androidTestExtension(libs) + testExtension(libs) + junitExtension(libs) +} + +secrets { + defaultPropertiesFileName = when { + project.rootProject.file("ci-overrides.properties").exists() -> "ci-overrides.properties" + else -> "gradle.properties" + } +} + diff --git a/scripts/src/main/kotlin/base-multiplatform-library.gradle.kts b/scripts/src/main/kotlin/base-multiplatform-library.gradle.kts new file mode 100644 index 00000000..99064980 --- /dev/null +++ b/scripts/src/main/kotlin/base-multiplatform-library.gradle.kts @@ -0,0 +1,135 @@ +@file:Suppress("UnusedPrivateProperty") + +import extensions.BuildNames.minifiedDebug +import extensions.BuildNames.targetDesktop +import extensions.BuildNames.versionCatalogLibrary +import extensions.Versions.BUILD_TOOLS_VERSION +import extensions.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET +import extensions.Versions.JavaVersion.PROJECT_JAVA_VERSION +import extensions.Versions.SdkVersions.COMPILE_SDK_VERSION +import extensions.Versions.SdkVersions.MIN_SDK_VERSION +import extensions.coroutinesCoreLibrary +import extensions.coroutinesTestBundle +import extensions.cryptoBundle +import extensions.databaseBundle +import extensions.datetimeBundle +import extensions.diKotlinBundle +import extensions.excludeList +import extensions.junitBundle +import extensions.kotlinTestBundle +import extensions.multiplatformPagingLibrary +import extensions.napierLibrary +import extensions.networkBundle +import extensions.networkOkhttpMockWebServerLibrary +import extensions.networkRetrofitLibrary +import extensions.serializationBundle + +plugins { + id("com.android.library") + id("kotlin-multiplatform") + id("com.codingfeline.buildkonfig") + kotlin("plugin.serialization") + id("quality-detekt") + id("io.realm.kotlin") + id("org.jetbrains.compose") + id("jacoco") +} + +tasks.withType { + useJUnitPlatform() +} + +// JaCoCo configuration for Desktop +tasks.register("jacocoTestReportKmm") { + dependsOn("desktopTest") + + reports { + xml.required.set(true) + html.required.set(true) + } + + val jvmClasses = fileTree("build/classes/kotlin/desktop/main").apply { + excludeList() + } + val jvmSourceFiles = files("src/desktopMain/kotlin") + classDirectories.setFrom(jvmClasses) + sourceDirectories.setFrom(jvmSourceFiles) + executionData.setFrom(fileTree("build").include("**/desktopTest.exec")) +} + +val libs: VersionCatalog = extensions.getByType().named(versionCatalogLibrary) + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = KOTLIN_OPTIONS_JVM_TARGET + } + } + } + jvm(targetDesktop) + sourceSets { + val commonMain by getting { + dependencies { + implementation(kotlin("reflect")) + implementation(libs.multiplatformPagingLibrary) + implementation(libs.coroutinesCoreLibrary) + implementation(libs.datetimeBundle) + implementation(libs.databaseBundle) + implementation(libs.cryptoBundle) + implementation(libs.serializationBundle) + implementation(libs.napierLibrary) + implementation(libs.networkBundle) + implementation(libs.diKotlinBundle) + } + } + val commonTest by getting { + dependencies { + implementation(libs.databaseBundle) + implementation(libs.coroutinesTestBundle) + implementation(libs.serializationBundle) + implementation(libs.kotlinTestBundle) + implementation(libs.junitBundle) + implementation(libs.cryptoBundle) + implementation(libs.datetimeBundle) + implementation(libs.networkRetrofitLibrary) + implementation(libs.networkOkhttpMockWebServerLibrary) + } + } + val desktopTest by getting { + dependencies { + dependsOn(commonTest) + implementation(libs.cryptoBundle) + implementation(libs.datetimeBundle) + } + } + } +} + +android { + buildToolsVersion = BUILD_TOOLS_VERSION + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + compileSdk = COMPILE_SDK_VERSION + defaultConfig { + minSdk = MIN_SDK_VERSION + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + compileOptions { + sourceCompatibility = PROJECT_JAVA_VERSION + targetCompatibility = PROJECT_JAVA_VERSION + } + buildTypes { + val debug by getting { + isJniDebuggable = true + enableUnitTestCoverage = true + enableAndroidTestCoverage = true + } + create(minifiedDebug) { + initWith(debug) + } + } + buildFeatures { + buildConfig = true + } +} diff --git a/scripts/src/main/kotlin/extensions/BuildFileData.kt b/scripts/src/main/kotlin/extensions/BuildFileData.kt new file mode 100644 index 00000000..f4dc6754 --- /dev/null +++ b/scripts/src/main/kotlin/extensions/BuildFileData.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package extensions + +import org.gradle.api.tasks.util.PatternFilterable + +internal object BuildNames { + const val targetDesktop = "desktop" + const val minifiedDebug = "minifiedDebug" + const val versionCatalogLibrary = "libs" + const val androidTestImplementation = "androidTestImplementation" + const val androidTestUtil = "androidTestUtil" + const val testImplementation = "testImplementation" + const val testCompileOnly = "testCompileOnly" + + // virtual devices + val pixel5Api30 = VirtualDevice("pixel5api30", "Pixel 5", 30, "aosp-atd") + val pixel8Api34 = VirtualDevice("pixel8api34", "Pixel 8", 34, "aosp-atd") + + const val jacocoToolsVersion = "0.8.7" +} + +internal data class VirtualDevice( + val name: String, + val device: String, + val apiLevel: Int, + val systemImageSource: String +) + +val sourcesKt = listOf( + "app/android/src/**/de/gematik/**/*.kt", + "app/android-mock/src/**/de/gematik/**/*.kt", + "app/demo-mode/src/**/de/gematik/**/*.kt", + "app/features/src/**/de/gematik/**/*.kt", + "app/features/src/**/android/print/**/*.kt", + "app/test-actions/src/**/de/gematik/**/*.kt", + "app/test-tags/src/**/de/gematik/**/*.kt", + "buildSrc/src/**/de/gematik/**/*.kt", + "common/src/**/de/gematik/**/*.kt", + "desktop/src/**/de/gematik/**/*.kt", + "smartcard-wrapper/src/**/de/gematik/**/*.kt", + "plugins/*/src/**/*.kt", + "rules/src/**/de/gematik/**/*.kt", + "scripts/src/**/*.kt" +) + +fun PatternFilterable.excludeList() = apply { + exclude("**/build/**") + exclude("build/**") + exclude("**/generated/**") + exclude("**/android/print/**") + exclude("**/de/gematik/ti/erp/app/**/ui/**") + exclude("**/de/gematik/ti/erp/**/navigation/*") + exclude("**/de/gematik/ti/erp/**/model/*") + exclude("**/de/gematik/ti/erp/**/components/*") + exclude("**/de/gematik/ti/erp/app/utils/*") + exclude("**/de/gematik/ti/erp/app/di/*") + exclude("**/de/gematik/ti/erp/app/**/di/*") + exclude("**/de/gematik/ti/erp/app/theme/*") + exclude("**/R.class") + exclude("**/R$*.class") + exclude("**/BuildConfig.*") + exclude("**/Manifest*.*") + exclude("android/**/*.*") + exclude("**/de/gematik/ti/erp/app/TestTags.class") + exclude("**/de/gematik/ti/erp/app/testactions/**") +} diff --git a/scripts/src/main/kotlin/extensions/DependencyCheckExtension.kt b/scripts/src/main/kotlin/extensions/DependencyCheckExtension.kt new file mode 100644 index 00000000..bca4a6d1 --- /dev/null +++ b/scripts/src/main/kotlin/extensions/DependencyCheckExtension.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package extensions + +import org.gradle.api.artifacts.ConfigurationContainer +import org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension +import org.owasp.dependencycheck.reporting.ReportGenerator + +fun DependencyCheckExtension.checks(configurations: ConfigurationContainer) { + analyzers.assemblyEnabled = false + suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" + formats = + listOf( + ReportGenerator.Format.HTML, + ReportGenerator.Format.XML + ) + scanConfigurations = configurations.filter { + it.name.startsWith("api") || + it.name.startsWith("implementation") || + it.name.startsWith("kapt") + }.map { it.name } + failBuildOnCVSS = Versions.CVSS.allowed +} diff --git a/scripts/src/main/kotlin/extensions/DependencyHandlerExtension.kt b/scripts/src/main/kotlin/extensions/DependencyHandlerExtension.kt new file mode 100644 index 00000000..319a4f0c --- /dev/null +++ b/scripts/src/main/kotlin/extensions/DependencyHandlerExtension.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package extensions + +import extensions.BuildNames.androidTestImplementation +import extensions.BuildNames.androidTestUtil +import extensions.BuildNames.testCompileOnly +import extensions.BuildNames.testImplementation +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.kotlin.dsl.DependencyHandlerScope +import org.gradle.kotlin.dsl.kotlin + +fun DependencyHandlerScope.androidTestExtension(libs: VersionCatalog) { + androidTestImplementation(kotlin("reflect")) + androidTestImplementation(kotlin("stdlib")) + androidTestImplementation(libs.diComposeLibrary) + androidTestImplementation(libs.networkOkHttpLibrary) + androidTestImplementation(libs.androidXTestBundle) + androidTestImplementation(libs.composeTestBundle) + androidTestImplementation(libs.mockkAndroidBundle) + androidTestUtil(libs.androidXTestUtilsBundle) +} + +fun DependencyHandlerScope.testExtension(libs: VersionCatalog) { + testImplementation(kotlin("test")) + testImplementation(libs.cryptoBundle) + testImplementation(libs.androidxTestArchCoreBundle) + testImplementation(libs.coroutinesTestBundle) + testImplementation(libs.networkTestBundle) + testImplementation(libs.composeTestBundle) + testImplementation(libs.testingBundle) + testCompileOnly(libs.databaseBundle) + testCompileOnly(libs.datetimeBundle) +} + +fun DependencyHandlerScope.junitExtension(libs: VersionCatalog) { + testImplementation(libs.junitBundle) +} diff --git a/scripts/src/main/kotlin/extensions/VersionCatalogBundles.kt b/scripts/src/main/kotlin/extensions/VersionCatalogBundles.kt new file mode 100644 index 00000000..33b0652f --- /dev/null +++ b/scripts/src/main/kotlin/extensions/VersionCatalogBundles.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("unused") + +package extensions + +import org.gradle.api.artifacts.ExternalModuleDependencyBundle +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.provider.Provider + +// Bundles +internal val VersionCatalog.accompanistBundle: Provider + get() = getBundle("accompanist") + +internal val VersionCatalog.androidxAppBundle: Provider + get() = getBundle("androidx-app") + +internal val VersionCatalog.androidxBundle: Provider + get() = getBundle("androidx") + +internal val VersionCatalog.androidxTestArchCoreBundle: Provider + get() = getBundle("androidx-test-arch-core") + +internal val VersionCatalog.animationBundle: Provider + get() = getBundle("animation") + +internal val VersionCatalog.composeAppBundle: Provider + get() = getBundle("compose-app") + +internal val VersionCatalog.composeBundle: Provider + get() = getBundle("compose") + +internal val VersionCatalog.composeTestBundle: Provider + get() = getBundle("composetest") + +internal val VersionCatalog.cameraBundle: Provider + get() = getBundle("camera") + +internal val VersionCatalog.coroutinesBundle: Provider + get() = getBundle("coroutines") + +internal val VersionCatalog.coroutinesTestBundle: Provider + get() = getBundle("corutinestest") + +internal val VersionCatalog.cryptoBundle: Provider + get() = getBundle("crypto") + +internal val VersionCatalog.databaseBundle: Provider + get() = getBundle("database") + +internal val VersionCatalog.datetimeBundle: Provider + get() = getBundle("datetime") + +internal val VersionCatalog.datamatrixBundle: Provider + get() = getBundle("datamatrix") + +internal val VersionCatalog.diBundle: Provider + get() = getBundle("di") + +internal val VersionCatalog.diKotlinBundle: Provider + get() = getBundle("di-kotlin") + +internal val VersionCatalog.diViewModelBundle: Provider + get() = getBundle("di-viewmodel") + +internal val VersionCatalog.imageBundle: Provider + get() = getBundle("image") + +internal val VersionCatalog.junitBundle: Provider + get() = getBundle("testjunit") + +internal val VersionCatalog.kotlinBundle: Provider + get() = getBundle("kotlin") + +internal val VersionCatalog.kotlinTestBundle: Provider + get() = getBundle("kotlintest") + +internal val VersionCatalog.lifecycleBundle: Provider + get() = getBundle("lifecycle") + +internal val VersionCatalog.loggingBundle: Provider + get() = getBundle("logging") + +internal val VersionCatalog.mapsBundle: Provider + get() = getBundle("maps") + +internal val VersionCatalog.networkBundle: Provider + get() = getBundle("network") + +internal val VersionCatalog.networkTestBundle: Provider + get() = getBundle("networktest") + +internal val VersionCatalog.othersBundle: Provider + get() = getBundle("others") + +internal val VersionCatalog.pdfboxBundle: Provider + get() = getBundle("pdfbox") + +internal val VersionCatalog.playBundle: Provider + get() = getBundle("play") + +internal val VersionCatalog.processPhoenixBundle: Provider + get() = getBundle("processphoenix") + +internal val VersionCatalog.serializationBundle: Provider + get() = getBundle("serialization") + +internal val VersionCatalog.trackingBundle: Provider + get() = getBundle("tracking") + +internal val VersionCatalog.testingBundle: Provider + get() = getBundle("testing") + +internal val VersionCatalog.androidXTestBundle: Provider + get() = getBundle("androidxtest") + +internal val VersionCatalog.androidXTestUtilsBundle: Provider + get() = getBundle("androidxtestutils") + +internal val VersionCatalog.mockkAndroidBundle: Provider + get() = getBundle("mockandroid") + +internal val VersionCatalog.detektComposeRules: Provider + get() = getBundle("qualitydetektcomposerules") + +private fun VersionCatalog.getBundle(bundle: String) = findBundle(bundle).get() diff --git a/scripts/src/main/kotlin/extensions/VersionCatalogLibs.kt b/scripts/src/main/kotlin/extensions/VersionCatalogLibs.kt new file mode 100644 index 00000000..77a5c1f3 --- /dev/null +++ b/scripts/src/main/kotlin/extensions/VersionCatalogLibs.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("unused") + +package extensions + +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.provider.Provider + +internal val VersionCatalog.androidxCoreKtxLibrary: Provider + get() = getLibrary("androix-core-ktx") + +internal val VersionCatalog.coroutinesCoreLibrary: Provider + get() = getLibrary("kotlinx-coroutines-core") + +internal val VersionCatalog.desugarLibrary: Provider + get() = getLibrary("desugar_jdk_libs") + +internal val VersionCatalog.diComposeLibrary: Provider + get() = getLibrary("di-compose") + +internal val VersionCatalog.kotlinReflectLibrary: Provider + get() = getLibrary("kotlin-reflect") + +internal val VersionCatalog.ktLintLibrary: Provider + get() = getLibrary("quality-ktlint") + +internal val VersionCatalog.detektLibrary: Provider + get() = getLibrary("quality-detekt") + +internal val VersionCatalog.multiplatformPagingLibrary: Provider + get() = getLibrary("paging-common-ktx") + +internal val VersionCatalog.napierLibrary: Provider + get() = getLibrary("logging-napier") + +internal val VersionCatalog.materialLibrary: Provider + get() = getLibrary("material") + +internal val VersionCatalog.navigationComposeLibrary: Provider + get() = getLibrary("navigation-compose") + +internal val VersionCatalog.networkOkHttpLibrary: Provider + get() = getLibrary("network-okhttp") + +internal val VersionCatalog.networkRetrofitLibrary: Provider + get() = getLibrary("network-retrofit") + +internal val VersionCatalog.networkOkhttpMockWebServerLibrary: Provider + get() = getLibrary("network-okhttp-mockwebserver") + +internal val VersionCatalog.playAppUpdateLibrary: Provider + get() = getLibrary("play-app-update") + +internal val VersionCatalog.gematikRulesLibrary: Provider + get() = getLibrary("rules") + +private fun VersionCatalog.getLibrary(library: String) = findLibrary(library).get() diff --git a/scripts/src/main/kotlin/extensions/Versions.kt b/scripts/src/main/kotlin/extensions/Versions.kt new file mode 100644 index 00000000..0650519a --- /dev/null +++ b/scripts/src/main/kotlin/extensions/Versions.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package extensions + +internal object Versions { + internal object SdkVersions { + const val MIN_SDK_VERSION = 26 // Android 8.0 + const val COMPILE_SDK_VERSION = 35 + const val TARGET_SDK_VERSION = 35 + } + + internal const val BUILD_TOOLS_VERSION = "34.0.0" + internal const val KOTLIN_COMPILER_EXTENSION_VERSION = "1.5.3" + + internal object JavaVersion { + const val KOTLIN_OPTIONS_JVM_TARGET = "17" + val PROJECT_JAVA_VERSION = org.gradle.api.JavaVersion.VERSION_17 + } + + internal object CVSS { + const val allowed = 6.9f + } +} diff --git a/scripts/src/main/kotlin/quality/quality-detekt.gradle.kts b/scripts/src/main/kotlin/quality/quality-detekt.gradle.kts new file mode 100644 index 00000000..754a28e5 --- /dev/null +++ b/scripts/src/main/kotlin/quality/quality-detekt.gradle.kts @@ -0,0 +1,44 @@ +import extensions.detektComposeRules +import extensions.sourcesKt +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.withType + +val libs: VersionCatalog = extensions.getByType().named("libs") + +apply() + +dependencies { + "detektPlugins"(libs.detektComposeRules) +} + +@Suppress("SpreadOperator") +configure { + autoCorrect = false + source = fileTree(rootDir) { + include(sourcesKt) + }.filter { it.extension != "kts" } + .map { it.parentFile } + .let { + files(*it.toTypedArray()) + } + parallel = true + config = files("${rootDir}/config/detekt/detekt.yml") + baseline = file("${rootDir}config/detekt/baseline.xml") + buildUponDefaultConfig = false + allRules = false + disableDefaultRuleSets = false + debug = false + ignoreFailures = false +} + +tasks.withType().configureEach { + exclude("**/resources/**,**/build/**") +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 56ebef59..712412d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,8 @@ +@file:Suppress("UnstableApiUsage") + +import java.util.Properties + + pluginManagement { repositories { maven("https://oss.sonatype.org/content/repositories/snapshots/") @@ -5,6 +10,7 @@ pluginManagement { google() gradlePluginPortal() mavenCentral() + maven("https://jitpack.io") } resolutionStrategy { eachPlugin { @@ -13,20 +19,47 @@ pluginManagement { } } } - - includeBuild("plugins/dependencies") - includeBuild("plugins/resource-generation") + includeBuild("scripts") includeBuild("plugins/technical-requirements-plugin") + // needed only for desktop app + // includeBuild("plugins/dependencies") + // includeBuild("plugins/resource-generation") } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + + // decide if obtaining dependencies from nexus + val properties = Properties() + try { + properties.load(File("ci-overrides.properties").inputStream()) + } catch (e: Exception) { + println("Could not load ci-overrides.properties") + } + + val nexusUsername: String? = properties.getProperty("NEXUS_USERNAME") + val nexusPassword: String? = properties.getProperty("NEXUS_PASSWORD") + val nexusUrl: String? = properties.getProperty("NEXUS_URL") + val obtainFromNexus = !nexusUrl.isNullOrEmpty() && !nexusUsername.isNullOrEmpty() && !nexusPassword.isNullOrEmpty() + repositories { maven("https://oss.sonatype.org/content/repositories/snapshots/") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() mavenCentral() maven("https://jitpack.io") + + if (obtainFromNexus) { + maven { + name = "nexus" + setUrl(nexusUrl!!) + credentials { + username = nexusUsername + password = nexusPassword + } + } + } } } @@ -36,11 +69,12 @@ includeBuild("rules") { } } -includeBuild("smartcard-wrapper") { - dependencySubstitution { - substitute(module("de.gematik.ti.erp.app:smartcard-wrapper")).using(project(":")) - } -} +// needed only for desktop app +//includeBuild("smartcard-wrapper") { +// dependencySubstitution { +// substitute(module("de.gematik.ti.erp.app:smartcard-wrapper")).using(project(":")) +// } +//} // includeBuild("modules/fhir-parser") { // dependencySubstitution { @@ -51,10 +85,13 @@ includeBuild("smartcard-wrapper") { include(":app:android") include(":app:android-mock") include(":app:features") -include(":app:shared-test") +include(":app:test-actions") +include(":app:test-tags") include(":app:demo-mode") +include(":ui-components") include(":common") -include(":desktop") include(":plugins:technical-requirements-plugin") +// needed only for desktop app +// include(":desktop") rootProject.name = "E-Rezept" diff --git a/smartcard-wrapper/build.gradle.kts b/smartcard-wrapper/build.gradle.kts deleted file mode 100644 index 2943363e..00000000 --- a/smartcard-wrapper/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - kotlin("jvm") version "1.7.0" -} - -version = 1.0 -group = "de.gematik.ti.erp.app" - -tasks.withType { - kotlinOptions.jvmTarget = "17" - kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" -} - -dependencies { - implementation(kotlin("stdlib")) -} diff --git a/smartcard-wrapper/settings.gradle.kts b/smartcard-wrapper/settings.gradle.kts deleted file mode 100644 index 240be2f2..00000000 --- a/smartcard-wrapper/settings.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -pluginManagement { - repositories { - google() - gradlePluginPortal() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) - repositories { - google() - mavenCentral() - } -} diff --git a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Card.kt b/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Card.kt deleted file mode 100644 index 5bca8c5b..00000000 --- a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Card.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.smartcard - -import java.io.Closeable -import java.io.IOException -import java.nio.ByteBuffer -import java.util.Objects -import javax.smartcardio.Card -import javax.smartcardio.CardChannel - -class CardException internal constructor(cause: javax.smartcardio.CardException) : IOException(cause) - -class Card internal constructor(private val reference: Card) : Closeable { - init { - reference.beginExclusive() - } - - private val channel: CardChannel = reference.basicChannel - - fun transmit(command: ByteBuffer, response: ByteBuffer): Int = - try { - Objects.requireNonNull(channel).transmit(command, response) - } catch (e: javax.smartcardio.CardException) { - throw CardException(e) - } - - override fun close() { - try { - reference.endExclusive() - reference.disconnect(true) - } catch (e: javax.smartcardio.CardException) { - throw CardException(e) - } - } -} diff --git a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardFactory.kt b/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardFactory.kt deleted file mode 100644 index 12c5cb27..00000000 --- a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardFactory.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.smartcard - -import javax.smartcardio.TerminalFactory - -class CardFactory private constructor() { - private val factory: TerminalFactory by lazy { - TerminalFactory.getInstance("PC/SC", null) - } - - val readers: List - get() = try { - factory.terminals().list().map { CardReader(it) } - } catch (e: javax.smartcardio.CardException) { - throw CardException(e) - } - - companion object { - val instance: CardFactory by lazy { - Workarounds.`workaround for MacOSX Big Sur And Monterey - PCSC not found bug`() - - CardFactory() - } - } -} diff --git a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardReader.kt b/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardReader.kt deleted file mode 100644 index e45164cb..00000000 --- a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/CardReader.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.smartcard - -import javax.smartcardio.CardTerminal - -class CardReader internal constructor(private val reference: CardTerminal) { - val name: String = reference.name - - val isCardPresent: Boolean - get() = try { - reference.isCardPresent - } catch (e: javax.smartcardio.CardException) { - throw CardException(e) - } - - fun connect(): Card = - try { - Card(reference.connect("*")) - } catch (e: javax.smartcardio.CardException) { - throw CardException(e) - } -} diff --git a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Workarounds.kt b/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Workarounds.kt deleted file mode 100644 index 38bff582..00000000 --- a/smartcard-wrapper/src/main/kotlin/de/gematik/ti/erp/app/smartcard/Workarounds.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.smartcard - -internal object Workarounds { - // workaround for https://bugs.openjdk.java.net/browse/JDK-8255877 - fun `workaround for MacOSX Big Sur And Monterey - PCSC not found bug`() { - val javaVersion = System.getProperty("java.version") - val majorJavaVersion = javaVersion.substring(0, javaVersion.indexOf('.')).toInt() - val osVersion = System.getProperty("os.version") - val osName = System.getProperty("os.name") - val majorOsVersion = osVersion.substring(0, osVersion.indexOf('.')).toInt() - if (osName == "Mac OS X" && majorJavaVersion <= 16 && (majorOsVersion == 11 || majorOsVersion == 12)) { - System.setProperty( - "sun.security.smartcardio.library", - "/System/Library/Frameworks/PCSC.framework/Versions/Current/PCSC" - ) - } - } -} diff --git a/smartcard-wrapper/src/main/kotlin/module-info.java b/smartcard-wrapper/src/main/kotlin/module-info.java deleted file mode 100644 index 3180d0c6..00000000 --- a/smartcard-wrapper/src/main/kotlin/module-info.java +++ /dev/null @@ -1,4 +0,0 @@ -module E.Rezept.Desktop.pcsc { - requires kotlin.stdlib; - requires java.smartcardio; -} diff --git a/ui-components/.gitignore b/ui-components/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/ui-components/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ui-components/build.gradle.kts b/ui-components/build.gradle.kts new file mode 100644 index 00000000..c82e8077 --- /dev/null +++ b/ui-components/build.gradle.kts @@ -0,0 +1,19 @@ +import de.gematik.ti.erp.app.plugins.names.AppDependencyNamesPlugin + +plugins { + id("base-android-library") + id("de.gematik.ti.erp.names") +} + +val gematik = AppDependencyNamesPlugin() + +android { + namespace = gematik.moduleName("ui_components") + defaultConfig { + testApplicationId = gematik.moduleName("ui_components.test") + } +} + +dependencies { + implementation(libs.compose.ui) +} diff --git a/ui-components/consumer-rules.pro b/ui-components/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/ui-components/proguard-rules.pro b/ui-components/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/ui-components/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/ui-components/src/main/AndroidManifest.xml b/ui-components/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e1000761 --- /dev/null +++ b/ui-components/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimateTime.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimateTime.kt new file mode 100644 index 00000000..93a2a047 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimateTime.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.animated + +data object AnimationTime { + const val DELAY_250 = 250 + const val DELAY_100 = 100 + const val SHORT_DELAY = 250L + const val SHORT_L = 300L + const val LONG = 500 + const val ONE_SECOND = 1000 +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimatedComponent.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimatedComponent.kt new file mode 100644 index 00000000..904f6fe8 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/animated/AnimatedComponent.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.animated + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay + +/** + * Animate the content with fadeIn and slideInVertically + */ +@Composable +fun AnimatedComponent( + content: @Composable () -> Unit +) { + var visible by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(AnimationTime.SHORT_L) + visible = true + } + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(AnimationTime.LONG)) + + slideInVertically(initialOffsetY = { -it }, animationSpec = tween(AnimationTime.LONG)) + ) { + content() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/column/ColumnItems.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/column/ColumnItems.kt new file mode 100644 index 00000000..8d162a85 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/column/ColumnItems.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.column + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable + +/** + * [items] refers to the list of items to be displayed + * [itemContent] is for placing the content of the item + * [lastItemExtraContent] is for placing the extra content below the last item + */ +@Composable +fun ColumnItems( + items: List, + itemContent: @Composable ColumnScope.(Int, T) -> Unit, + lastItemExtraContent: @Composable ColumnScope.(Int, T) -> Unit +) { + Column { + items.forEachIndexed { index, item -> + if (index == items.size - 1) { + itemContent(index, item) + lastItemExtraContent(index, item) + } else { + itemContent(index, item) + } + } + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/extensions/Dp.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/extensions/Dp.kt new file mode 100644 index 00000000..187feae5 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/extensions/Dp.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.extensions + +import android.content.res.Resources +import androidx.compose.ui.unit.Dp + +fun Dp.toPx(): Float { + val density = Resources.getSystem().displayMetrics.density + return this.value * density +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/labels/InfoLabel.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/labels/InfoLabel.kt new file mode 100644 index 00000000..62f7c21d --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/labels/InfoLabel.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.labels + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AutoAwesome +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerMedium + +@Suppress("MagicNumber") +@Composable +fun InfoLabel( + modifier: Modifier = Modifier, + text: String, + infoIcon: ImageVector = Icons.Outlined.AutoAwesome, + closeIcon: ImageVector = Icons.Outlined.Close, + contentColor: Color = AppTheme.colors.primary900, + containerColor: Color = AppTheme.colors.primary100, + onClose: () -> Unit +) { + OutlinedCard( + modifier = modifier, + colors = CardDefaults.outlinedCardColors( + contentColor = contentColor, + containerColor = containerColor + ), + border = BorderStroke(SizeDefaults.sixteenth, contentColor) + ) { + Row( + modifier = Modifier + .height(SizeDefaults.sevenfoldAndHalf) // 60.dp + .padding(PaddingDefaults.Small) + ) { + Icon( + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterVertically) + .weight(0.07f), + imageVector = infoIcon, + contentDescription = null + ) + SpacerMedium() + Box( + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterVertically) + .weight(0.8f), + contentAlignment = Alignment.Center + ) { + Text( + text = text + ) + } + SpacerMedium() + IconButton( + modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterVertically) + .weight(0.07f), + onClick = onClose + ) { + Icon( + imageVector = closeIcon, + contentDescription = null + ) + } + } + } +} + +@Composable +fun BoxScope.InfoLabelInBox( + text: String, + onClose: () -> Unit +) { + Box( + modifier = Modifier + .padding(top = PaddingDefaults.Medium) + .padding(horizontal = PaddingDefaults.Large) + .fillMaxWidth() + .align(Alignment.TopCenter), + contentAlignment = Alignment.TopCenter + ) { + InfoLabel( + text = text, + onClose = onClose + ) + } +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun InfoLabelPreview() { + PreviewTheme { + InfoLabel( + text = """ + Information that is to be shown in the info label. More text to make the text bigger + """.trimIndent() + ) {} + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/launchedeffect/LaunchedEffectOnStart.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/launchedeffect/LaunchedEffectOnStart.kt new file mode 100644 index 00000000..e115c30a --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/launchedeffect/LaunchedEffectOnStart.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.launchedeffect + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect + +@Composable +fun LaunchedEffectOnStart( + content: () -> Unit +) { + LaunchedEffect(true) { + content() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/loading/LoadingIndicator.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/loading/LoadingIndicator.kt new file mode 100644 index 00000000..a3a2f635 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/loading/LoadingIndicator.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.loading + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.compose.fullscreen.Center + +@Composable +fun LoadingIndicator() { + Center { + CircularProgressIndicator( + modifier = Modifier.size(SizeDefaults.doubleHalf), + color = AppTheme.colors.primary400 + ) + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/LightDarkPreview.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/LightDarkPreview.kt new file mode 100644 index 00000000..d0fdbfe6 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/LightDarkPreview.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.preview + +import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Preview + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true +) +private annotation class LightPreview + +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true +) +private annotation class DarkPreview + +@LightPreview +@DarkPreview +internal annotation class LightDarkPreview diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/PreviewAppTheme.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/PreviewAppTheme.kt new file mode 100644 index 00000000..844fcebe --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/preview/PreviewAppTheme.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.preview + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme + +@Composable +internal fun PreviewTheme( + modifier: Modifier = Modifier, + content: @Composable () -> Unit + +) { + AppTheme { + Surface( + modifier = modifier + ) { + content() + } + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefresh.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefresh.kt new file mode 100644 index 00000000..472c4be3 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefresh.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pulltorefresh + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PullToRefresh( + modifier: Modifier = Modifier, + pullToRefreshState: PullToRefreshState, + color: Color = AppTheme.colors.primary400 +) { + // Surface is not used here, as we do not want its input-blocking behaviour, since the indicator + // is typically displayed above other (possibly) interactive indicator. + Box( + modifier = modifier + .size(SizeDefaults.fivefold) + .graphicsLayer { + translationY = pullToRefreshState.verticalOffset - size.height + } + .background(color = Color.Transparent, shape = CircleShape) + ) { + PullToRefreshIndicator( + state = pullToRefreshState, + color = color + ) + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefreshIndicator.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefreshIndicator.kt new file mode 100644 index 00000000..910d0bd2 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/PullToRefreshIndicator.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pulltorefresh + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.theme.AppTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun PullToRefreshIndicator( + state: PullToRefreshState, + modifier: Modifier = Modifier, + containerColor: Color = AppTheme.colors.neutral000, + color: Color = AppTheme.colors.primary400 +) { + PullToRefreshContainer( + modifier = modifier, + state = state, + containerColor = containerColor, + contentColor = color + ) +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/extensions/PullToRefreshStateExtensions.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/extensions/PullToRefreshStateExtensions.kt new file mode 100644 index 00000000..22d9dddf --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/pulltorefresh/extensions/PullToRefreshStateExtensions.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.pulltorefresh.extensions + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import de.gematik.ti.erp.app.shared.REFRESH_DELAY +import kotlinx.coroutines.delay + +@Suppress("ComposableNaming") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PullToRefreshState.trigger( + onStartRefreshing: (() -> Unit)? = null, + onNavigation: (() -> Unit)? = null, + block: suspend () -> Unit +) { + LaunchedEffect(isRefreshing) { + if (isRefreshing) { + onStartRefreshing?.invoke() + delay(REFRESH_DELAY) + block() + onNavigation?.invoke() // can be done better + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +fun PullToRefreshState.triggerStart() { + if (!isRefreshing) { + startRefresh() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +fun PullToRefreshState.triggerEnd() { + if (isRefreshing) { + endRefresh() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/semantics/SemanticsExtension.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/semantics/SemanticsExtension.kt new file mode 100644 index 00000000..717988ce --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/semantics/SemanticsExtension.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.semantics + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics + +fun Modifier.semanticsMergeDescendants( + properties: (SemanticsPropertyReceiver.() -> Unit) +) = semantics( + mergeDescendants = true, + properties = properties +) + +fun Modifier.semanticsHeading() = semantics { heading() } + +fun Modifier.semanticsMergedButton() = semantics(mergeDescendants = true) { role = Role.Button } + +fun Modifier.semanticsButton() = semantics { role = Role.Button } diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shared/Constants.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shared/Constants.kt new file mode 100644 index 00000000..489d15af --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shared/Constants.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shared + +internal const val INT_500 = 500 +internal const val REFRESH_DELAY = 500L +internal const val LIGHT_ALPHA = 0.2F diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/CircularShapeShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/CircularShapeShimmer.kt new file mode 100644 index 00000000..1e89b878 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/CircularShapeShimmer.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.shared.LIGHT_ALPHA +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun CircularShapeShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + alpha: Float = LIGHT_ALPHA, + size: Dp = SizeDefaults.fivefold +) { + Box( + modifier = Modifier + .then(modifier) + .size(size) + .clip(CircleShape) + .background(background) + .alpha(alpha) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun CircularShapeShimmerPreview() { + PreviewTheme { + CircularShapeShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LabelShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LabelShimmer.kt new file mode 100644 index 00000000..fd96791d --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LabelShimmer.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun LabelShimmer( + modifier: Modifier = Modifier +) { + Spacer( + modifier = Modifier + .then(modifier) + .clip(CircleShape) + .height(SizeDefaults.oneHalf) + .width(SizeDefaults.eightfoldAndHalf) + .background(AppTheme.colors.neutral400) + ) +} + +@Suppress("MagicNumber") +@Composable +fun LabelWithIconShimmer( + modifier: Modifier = Modifier +) { + Row(modifier = modifier) { + LabelShimmer( + modifier = Modifier + .weight(0.5f) + .padding(top = SizeDefaults.fivefoldHalf) + ) + Spacer( + modifier = Modifier.weight(0.25f) + ) + Icon( + modifier = Modifier.weight(0.25f), + imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight, + contentDescription = null, + tint = AppTheme.colors.neutral400 + ) + } +} + +@Suppress("UnusedPrivateMember") +@Composable +@LightDarkPreview +private fun LabelShimmerPreview() { + PreviewTheme { + LabelShimmer() + } +} + +@Suppress("UnusedPrivateMember") +@Composable +@Preview +private fun LabelWithIconShimmerPreview() { + PreviewTheme { + LabelWithIconShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LimitedTextShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LimitedTextShimmer.kt new file mode 100644 index 00000000..3d3a4639 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/LimitedTextShimmer.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun LimitedTextShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + width: Dp = SizeDefaults.fifteenfold +) { + Spacer( + modifier = Modifier + .then(modifier) + .height(SizeDefaults.oneHalf) + .width(width) + .background(background) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun LimitedTextShimmerPreview() { + PreviewTheme { + LimitedTextShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/MessageOverviewItemShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/MessageOverviewItemShimmer.kt new file mode 100644 index 00000000..7db0047b --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/MessageOverviewItemShimmer.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall + +@Suppress("MagicNumber") +@Composable +fun MessageOverviewItemShimmer() { + Column( + modifier = Modifier + .padding(bottom = PaddingDefaults.XXLarge) + .padding(horizontal = PaddingDefaults.Medium) + ) { + LimitedTextShimmer( + width = SizeDefaults.twentyfold + ) + SpacerSmall() + Row { + LimitedTextShimmer( + modifier = Modifier.weight(0.5f), + width = SizeDefaults.twentyfourfold + ) + Spacer( + modifier = Modifier.weight(0.25f) + ) + LabelWithIconShimmer( + modifier = Modifier.weight(0.25f) + ) + } + SpacerSmall() + LimitedTextShimmer( + background = AppTheme.colors.neutral300, + width = SizeDefaults.fifteenfold + ) + } +} + +@Suppress("UnusedPrivateMember") +@Composable +@LightDarkPreview +private fun MessageOverviewItemShimmerPreview() { + PreviewTheme { + MessageOverviewItemShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RectangularShapeShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RectangularShapeShimmer.kt new file mode 100644 index 00000000..ce7c999d --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RectangularShapeShimmer.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.shared.LIGHT_ALPHA +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun RectangularShapeShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + alpha: Float = LIGHT_ALPHA, + height: Dp = SizeDefaults.triple, + width: Dp = SizeDefaults.elevenfold +) { + Box( + modifier = Modifier + .then(modifier) + .height(height) + .width(width) + .clip(RoundedCornerShape(SizeDefaults.one)) + .background(background) + .alpha(alpha) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun RectangularShapeShimmerPreview() { + PreviewTheme { + RectangularShapeShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RowTextShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RowTextShimmer.kt new file mode 100644 index 00000000..d3ea14d0 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/RowTextShimmer.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun RowTextShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200 +) { + Spacer( + modifier = Modifier + .height(SizeDefaults.oneHalf) + .fillMaxWidth() + .then(modifier) + .background(background) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun RowTextShimmerPreview() { + PreviewTheme { + RowTextShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/SquareShapeShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/SquareShapeShimmer.kt new file mode 100644 index 00000000..79bca6af --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/SquareShapeShimmer.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.shared.LIGHT_ALPHA +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun SquareShapeShimmer( + modifier: Modifier = Modifier, + background: Color = AppTheme.colors.primary200, + alpha: Float = LIGHT_ALPHA, + size: Dp = SizeDefaults.fivefold +) { + Box( + modifier = Modifier + .then(modifier) + .size(size) + .clip(RoundedCornerShape(SizeDefaults.one)) + .background(background) + .alpha(alpha) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun SquareShapeShimmerPreview() { + PreviewTheme { + SquareShapeShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/StatusChipShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/StatusChipShimmer.kt new file mode 100644 index 00000000..ec038371 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/StatusChipShimmer.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults +import de.gematik.ti.erp.app.utils.SpacerSmall + +@Composable +fun StatusChipShimmer( + background: Color = AppTheme.colors.primary200 +) { + val shape = RoundedCornerShape(SizeDefaults.double) + Row( + Modifier + .background(background, shape) + .padding(SizeDefaults.eighth) + .clip(shape), + verticalAlignment = Alignment.CenterVertically + ) { + TinyTextShimmer() + SpacerSmall() + SquareShapeShimmer(size = SizeDefaults.eighth) + } +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun StatusChipShimmerPreview() { + PreviewTheme { + StatusChipShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/TinyTextShimmer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/TinyTextShimmer.kt new file mode 100644 index 00000000..a2cfae61 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/shimmer/TinyTextShimmer.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.shimmer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import de.gematik.ti.erp.app.preview.LightDarkPreview +import de.gematik.ti.erp.app.preview.PreviewTheme +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.SizeDefaults + +@Composable +fun TinyTextShimmer( + background: Color = AppTheme.colors.primary200 +) { + Spacer( + modifier = Modifier + .height(SizeDefaults.oneHalf) + .width(SizeDefaults.sevenfoldAndHalf) + .background(background) + ) +} + +@Suppress("UnusedPrivateMember") +@LightDarkPreview +@Composable +private fun TinyTextShimmerPreview() { + PreviewTheme { + TinyTextShimmer() + } +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt new file mode 100644 index 00000000..c97ea07e --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt @@ -0,0 +1,214 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class AppColors( + val green100: Color, + val green200: Color, + val green300: Color, + val green400: Color, + val green500: Color, + val green600: Color, + val green700: Color, + val green800: Color, + val green900: Color, + + val neutral000: Color, + val neutral025: Color, + val neutral050: Color, + val neutral100: Color, + val neutral200: Color, + val neutral300: Color, + val neutral400: Color, + val neutral500: Color, + val neutral600: Color, + val neutral700: Color, + val neutral800: Color, + val neutral900: Color, + val neutral999: Color, + + val primary100: Color, + val primary200: Color, + val primary300: Color, + val primary400: Color, + val primary500: Color, + val primary600: Color, + val primary700: Color, + val primary800: Color, + val primary900: Color, + + val red100: Color, + val red200: Color, + val red300: Color, + val red400: Color, + val red500: Color, + val red600: Color, + val red700: Color, + val red800: Color, + val red900: Color, + + val yellow100: Color, + val yellow200: Color, + val yellow300: Color, + val yellow400: Color, + val yellow500: Color, + val yellow600: Color, + val yellow700: Color, + val yellow800: Color, + val yellow900: Color, + + val scanOverlayErrorOutline: Color, + val scanOverlayErrorFill: Color, + + val scanOverlaySavedOutline: Color, + val scanOverlaySavedFill: Color, + + val scanOverlayHoldOutline: Color, + val scanOverlayHoldFill: Color +) + +val AppColorsThemeLight = AppColors( + green100 = Color(0xFFEBFFF0), + green200 = Color(0xFFC6F6D5), + green300 = Color(0xFF9AE6B4), + green400 = Color(0xFF68D391), + green500 = Color(0xFF48BB78), + green600 = Color(0xFF38A169), + green700 = Color(0xFF2F855A), + green800 = Color(0xFF276749), + green900 = Color(0xFF22543D), + + neutral000 = Color(0xFFFFFFFF), + neutral025 = Color(0xFFFDFDFD), + neutral050 = Color(0xFFFAFAFA), + neutral100 = Color(0xFFF5F5F5), + neutral200 = Color(0xFFEEEEEE), + neutral300 = Color(0xFFE0E0E0), + neutral400 = Color(0xFFBDBDBD), + neutral500 = Color(0xFF9E9E9E), + neutral600 = Color(0xFF757575), + neutral700 = Color(0xFF616161), + neutral800 = Color(0xFF424242), + neutral900 = Color(0xFF212121), + neutral999 = Color(0xFF000000), + + primary100 = Color(0xFFEBF8FF), + primary200 = Color(0xFFBEE3F8), + primary300 = Color(0xFF90CDF4), + primary400 = Color(0xFF63B3ED), + primary500 = Color(0xFF4299E1), + primary600 = Color(0xFF3182CE), + primary700 = Color(0xFF2B6CB0), + primary800 = Color(0xFF2C5282), + primary900 = Color(0xFF2A4365), + + red100 = Color(0xFFFFEBEB), + red200 = Color(0xFFFED7D7), + red300 = Color(0xFFFEB2B2), + red400 = Color(0xFFFC8181), + red500 = Color(0xFFF56565), + red600 = Color(0xFFE53E3E), + red700 = Color(0xFFC53030), + red800 = Color(0xFF9B2C2C), + red900 = Color(0xFF742A2A), + + yellow100 = Color(0xFFFFFFEB), + yellow200 = Color(0xFFFEFCBF), + yellow300 = Color(0xFFFAF089), + yellow400 = Color(0xFFF6E05E), + yellow500 = Color(0xFFECC94B), + yellow600 = Color(0xFFD69E2E), + yellow700 = Color(0xFFB7791F), + yellow800 = Color(0xFF975A16), + yellow900 = Color(0xFF744210), + + scanOverlayErrorOutline = Color(0xFFF86A6A), + scanOverlayErrorFill = Color(0x33E12D39), + scanOverlaySavedOutline = Color(0xFF00FF64), + scanOverlaySavedFill = Color(0x3300D353), + scanOverlayHoldOutline = Color(0xFFFFFFFF), + scanOverlayHoldFill = Color(0x26FFFFFF) +) + +val AppColorsThemeDark = AppColors( + green100 = Color(0x7F22543D), + green200 = Color(0xFF22543D), + green300 = Color(0xFF276749), + green400 = Color(0xFF2F855A), + green500 = Color(0xFF38A169), + green600 = Color(0xFF48BB78), + green700 = Color(0xFF68D391), + green800 = Color(0xFF9AE6B4), + green900 = Color(0xFFC6F6D5), + + neutral000 = Color(0xFF000000), + neutral025 = Color(0xFF0C0C0C), + neutral050 = Color(0xFF212121), + neutral100 = Color(0xFF424242), + neutral200 = Color(0xFF616161), + neutral300 = Color(0xFF757575), + neutral400 = Color(0xFF9E9E9E), + neutral500 = Color(0xFFBDBDBD), + neutral600 = Color(0xFFE0E0E0), + neutral700 = Color(0xFFEEEEEE), + neutral800 = Color(0xFFF5F5F5), + neutral900 = Color(0xFFFAFAFA), + neutral999 = Color(0xFFFFFFFF), + + primary100 = Color(0x7F33517A), + primary200 = Color(0xFF2A4365), + primary300 = Color(0xFF2C5282), + primary400 = Color(0xFF2B6CB0), + primary500 = Color(0xFF3182CE), + primary600 = Color(0xFF4299E1), + primary700 = Color(0xFF63B3ED), + primary800 = Color(0xFF90CDF4), + primary900 = Color(0xFFBEE3F8), + + red100 = Color(0x7F742A2A), + red200 = Color(0xFF742A2A), + red300 = Color(0xFF9B2C2C), + red400 = Color(0xFFC53030), + red500 = Color(0xFFE53E3E), + red600 = Color(0xFFF56565), + red700 = Color(0xFFFC8181), + red800 = Color(0xFFFEB2B2), + red900 = Color(0xFFFED7D7), + + yellow100 = Color(0x4CECC94B), + yellow200 = Color(0xFF744210), + yellow300 = Color(0xFF975A16), + yellow400 = Color(0xFFB7791F), + yellow500 = Color(0xFFD69E2E), + yellow600 = Color(0xFFECC94B), + yellow700 = Color(0xFFF6E05E), + yellow800 = Color(0xFFFAF089), + yellow900 = Color(0xFFFEFCBF), + + scanOverlayErrorOutline = Color(0xFFF86A6A), + scanOverlayErrorFill = Color(0x33E12D39), + scanOverlaySavedOutline = Color(0xFF00FF64), + scanOverlaySavedFill = Color(0x3300D353), + scanOverlayHoldOutline = Color(0xFFFFFFFF), + scanOverlayHoldFill = Color(0x26FFFFFF) +) diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Fonts.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Fonts.kt new file mode 100644 index 00000000..384d2da2 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Fonts.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.theme + +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import de.gematik.ti.erp.app.ui_components.R + +val fonts = FontFamily( + Font(R.font.noto_sans_bold, weight = FontWeight.Bold), + Font(R.font.noto_sans_medium, weight = FontWeight.Medium), + Font(R.font.noto_sans_regular, weight = FontWeight.Normal), + Font(R.font.noto_sans_semibold, weight = FontWeight.SemiBold) +) diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt new file mode 100644 index 00000000..44de7f97 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.theme + +import androidx.compose.ui.unit.dp + +object PaddingDefaults { + val Tiny = 4.dp + val Small = 8.dp + val ShortMedium = 12.dp + val Medium = 16.dp + val MediumPlus = 18.dp + val Large = 24.dp + val XLarge = 32.dp + val XXLarge = 40.dp + val XXLargeMedium = 56.dp + val XXXLarge = 112.dp +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt new file mode 100644 index 00000000..2e119740 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/SizeDefaults.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.theme + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.SizeDefaults.one + +object SizeDefaults { + /** `8dp` in design specification. */ + val one: Dp = 8.dp + + /** `0dp` in design specification. */ + val zero: Dp get() = multiple(0.0) + + /** `0.5dp` in design specification. */ + val sixteenth: Dp get() = multiple(.0625) + + /** `1dp` in design specification. */ + val eighth: Dp get() = multiple(.125) + + /** `2dp` in design specification. */ + val quarter: Dp get() = multiple(.25) + + /** `2.5dp` in design specification. */ + val fiveEighth: Dp get() = multiple(.3125) + + /** `3dp` in design specification. */ + val threeSeventyFifth: Dp get() = multiple(.375) + + /** `4dp` in design specification. */ + val half: Dp get() = multiple(.5) + + /** `5dp` in design specification. */ + val fivefoldHalf: Dp get() = multiple(0.625) + + /** `6dp` in design specification. */ + val threeQuarter: Dp get() = multiple(.75) + + /** `8dp` in design specification. */ + val onefold: Dp = 8.dp + + /** `10dp` in design specification. */ + val oneQuarter: Dp get() = multiple(1.25) + + /** `12dp` in design specification. */ + val oneHalf: Dp get() = multiple(1.5) + + /** `16dp` in design specification. */ + val double: Dp get() = multiple(2.0) + + /** `20dp` in design specification. */ + val doubleHalf: Dp get() = multiple(2.5) + + /** `22dp` in design specification. */ + val doubleThreeQuarter: Dp get() = multiple(2.75) + + /** `24dp` in design specification. */ + val triple: Dp get() = multiple(3.0) + + /** `28dp` in design specification. */ + val tripleHalf: Dp get() = multiple(3.5) + + /** `32dp` in design specification. */ + val fourfold: Dp get() = multiple(4.0) + + /** `36dp` in design specification. */ + val fourfoldAndHalf: Dp get() = multiple(4.5) + + /** `40dp` in design specification. */ + val fivefold: Dp get() = multiple(5.0) + + /** `48dp` in design specification. */ + val sixfold: Dp get() = multiple(6.0) + + /** `50dp` in design specification. */ + val sixfoldAndQuarter: Dp get() = multiple(6.25) + + /** `56dp` in design specification. */ + val sevenfold: Dp get() = multiple(7.0) + + /** `60dp` in design specification. */ + val sevenfoldAndHalf: Dp get() = multiple(7.5) + + /** `64dp` in design specification. */ + val eightfold: Dp get() = multiple(8.0) + + /** `66dp` in design specification. */ + val eightfoldAndHalf: Dp get() = multiple(8.5) + + /** `68dp` in design specification. */ + val eightfoldAndThreeQuarter: Dp get() = multiple(8.75) + + /** `72dp` in design specification. */ + val ninefold: Dp get() = multiple(9.0) + + /** `80dp` in design specification. */ + val tenfold: Dp get() = multiple(10.0) + + /** `88dp` in design specification. */ + val elevenfold: Dp get() = multiple(11.0) + + /** `96dp` in design specification. */ + val twelvefold: Dp get() = multiple(12.0) + + /** `104dp` in design specification. */ + val thirteenfold: Dp get() = multiple(13.0) + + /** `120dp` in design specification. */ + val fifteenfold: Dp get() = multiple(15.0) + + /** `120dp` in design specification. */ + val fifteenfoldAndHalf: Dp get() = multiple(15.5) + + /** `128dp` in design specification. */ + val sixteenfold: Dp get() = multiple(16.0) + + /** `144dp` in design specification. */ + val eighteenfold: Dp get() = multiple(18.0) + + /** `160dp` in design specification. */ + val twentyfold: Dp get() = multiple(20.0) + + /** `184dp` in design specification. */ + val twentythreefold: Dp get() = multiple(23.0) + + /** `192dp` in design specification. */ + val twentyfourfold: Dp get() = multiple(24.0) + + /** `200dp` in design specification. */ + val twentyfivefold: Dp get() = multiple(25.0) + + /** `304dp` in design specification. */ + val thirtyEightfold: Dp get() = multiple(38.0) + + /** `320dp` in design specification. */ + val fortyfold: Dp get() = multiple(40.0) + + /** `336dp` in design specification. */ + val fortyTwofold: Dp get() = multiple(42.0) + + /** `352dp` in design specification. */ + val fortyFourfold: Dp get() = multiple(44.0) + + /** `540dp` in design specification. */ + val sixtySevenfold: Dp get() = multiple(67.0) + + /** `640dp` in design specification. */ + val eightyfold: Dp get() = multiple(80.0) + + /** `680dp` in design specification. */ + val eightyFivefold: Dp get() = multiple(85.0) + + /** `700dp` in design specification. */ + val eightyEightfold: Dp get() = multiple(88.0) + + /** Default horizontal padding. */ + val defaultStartEnd: Dp get() = triple +} + +private fun multiple(multiplier: Float) = one * multiplier + +private fun multiple(multiplier: Double) = multiple(multiplier.toFloat()) diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt new file mode 100644 index 00000000..94439e05 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em + +@Suppress("LongMethod") +@Composable +fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) { + AppColorsThemeDark + } else { + AppColorsThemeLight + } + + val sizes = remember { SizeDefaults } + + val fontFamily = remember { fonts } + + val typoColors = AppTypographyColors( + body1l = colors.neutral600, + body2l = colors.neutral600, + subtitle1l = colors.neutral600, + subtitle2l = colors.neutral600, + captionl = colors.neutral600 + ) + + val materialTypo = MaterialTheme.typography.copy( + h1 = MaterialTheme.typography.h1.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W700 + ), + h2 = MaterialTheme.typography.h2.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W700 + ), + h3 = MaterialTheme.typography.h3.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W700 + ), + h4 = MaterialTheme.typography.h4.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W700 + ), + h5 = MaterialTheme.typography.h5.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W700 + ), + h6 = MaterialTheme.typography.h6.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em + ), + subtitle1 = MaterialTheme.typography.subtitle1.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W500 + ), + subtitle2 = MaterialTheme.typography.subtitle2.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em, + fontWeight = FontWeight.W500 + ), + body1 = MaterialTheme.typography.body1.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em + ), + body2 = MaterialTheme.typography.body2.copy( + fontFamily = fontFamily, + lineHeight = 1.5.em + ) + ) + + MaterialTheme( + typography = materialTypo, + colors = Colors( + primary = colors.primary600, + primaryVariant = colors.primary600, + secondary = colors.primary600, + secondaryVariant = colors.primary600, + background = colors.neutral025, + surface = colors.neutral000, + error = colors.red500, + onPrimary = colors.neutral000, + onSecondary = colors.neutral000, + onBackground = colors.neutral900, + onSurface = colors.neutral900, + onError = colors.red900, + isLight = !darkTheme + ), + content = { + val typo = + AppTypography( + body1l = MaterialTheme.typography.body1.copy( + fontFamily = fontFamily, + color = typoColors.body1l, + lineHeight = 1.5.em + ), + body2l = MaterialTheme.typography.body2.copy( + fontFamily = fontFamily, + color = typoColors.body2l, + lineHeight = 1.5.em + ), + subtitle1l = MaterialTheme.typography.subtitle1.copy( + fontFamily = fontFamily, + color = typoColors.subtitle1l, + lineHeight = 1.5.em + ), + subtitle2l = MaterialTheme.typography.subtitle2.copy( + fontFamily = fontFamily, + color = typoColors.subtitle2l, + lineHeight = 1.5.em + ), + caption1l = MaterialTheme.typography.caption.copy( + fontFamily = fontFamily, + color = typoColors.captionl, + lineHeight = 1.5.em + ), + h1 = materialTypo.h1, + h2 = materialTypo.h2, + h3 = materialTypo.h3, + h4 = materialTypo.h4, + h5 = materialTypo.h5, + h6 = materialTypo.h6, + subtitle1 = materialTypo.subtitle1, + subtitle2 = materialTypo.subtitle2, + body1 = materialTypo.body1, + body2 = materialTypo.body2, + button = materialTypo.button, + caption1 = materialTypo.caption, + caption2 = materialTypo.caption.copy(fontWeight = FontWeight.Medium), + overline = materialTypo.overline + ) + + CompositionLocalProvider( + LocalAppColors provides colors, + LocalAppTypographyColors provides typoColors, + LocalAppTypography provides typo, + LocalSizes provides sizes, + LocalFonts provides fonts, + content = content + ) + } + ) +} + +object AppTheme { + val colors: AppColors + @Composable + get() = LocalAppColors.current + + val typography: AppTypography + @Composable + get() = LocalAppTypography.current + + val DebugColor = Color(0xFFD71F5F) +} + +val LocalAppColors = staticCompositionLocalOf { + error("No AppColors provided") +} + +val LocalAppTypographyColors = staticCompositionLocalOf { + error("No AppTypographyColors provided") +} + +val LocalAppTypography = staticCompositionLocalOf { + error("No AppTypography provided") +} + +val LocalSizes = staticCompositionLocalOf { + error("No sized provided") +} + +val LocalFonts = staticCompositionLocalOf { + error("No font provided") +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt new file mode 100644 index 00000000..2288fc86 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +@file:Suppress("LongParameterList") + +package de.gematik.ti.erp.app.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle + +@Immutable +data class AppTypographyColors( + val body1l: Color, + val body2l: Color, + val subtitle1l: Color, + val subtitle2l: Color, + val captionl: Color +) + +@Immutable +class AppTypography( + // overloaded fonts with different lighter color + val body1l: TextStyle, + val body2l: TextStyle, + val subtitle1l: TextStyle, + val subtitle2l: TextStyle, + val caption1l: TextStyle, + // material theme default fonts + val h1: TextStyle, + val h2: TextStyle, + val h3: TextStyle, + val h4: TextStyle, + val h5: TextStyle, + val h6: TextStyle, + val subtitle1: TextStyle, + val subtitle2: TextStyle, + val body1: TextStyle, + val body2: TextStyle, + val button: TextStyle, + val caption1: TextStyle, + val caption2: TextStyle, + val overline: TextStyle +) diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/Spacer.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/Spacer.kt new file mode 100644 index 00000000..74885c11 --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/Spacer.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun SpacerTiny() = + Spacer(modifier = Modifier.size(PaddingDefaults.Tiny)) + +@Composable +fun SpacerSmall() = + Spacer(modifier = Modifier.size(PaddingDefaults.Small)) + +@Composable +fun SpacerMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.Medium)) + +@Composable +fun SpacerShortMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.ShortMedium)) + +@Composable +fun SpacerLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.Large)) + +@Composable +fun SpacerXLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.XLarge)) + +@Composable +fun SpacerXXLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.XXLarge)) + +@Composable +fun SpacerXXLargeMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.XXLargeMedium)) + +@Composable +fun SpacerXXXLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.XXXLarge)) diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/Center.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/Center.kt new file mode 100644 index 00000000..fdf0abef --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/Center.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.fullscreen + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun Center( + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + content = content + ) +} diff --git a/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/FullScreenLoadingIndicator.kt b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/FullScreenLoadingIndicator.kt new file mode 100644 index 00000000..79771daa --- /dev/null +++ b/ui-components/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/fullscreen/FullScreenLoadingIndicator.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2024, gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the + * European Commission – subsequent versions of the EUPL (the "Licence"). + * You may not use this work except in compliance with the Licence. + * + * You find a copy of the Licence in the "Licence" file or at + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. + * In case of changes by gematik find details in the "Readme" file. + * + * See the Licence for the specific language governing permissions and limitations under the Licence. + */ + +package de.gematik.ti.erp.app.utils.compose.fullscreen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun FullScreenLoadingIndicator() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { CircularProgressIndicator() } +} diff --git a/desktop/src/jvmMain/resources/fonts/NotoSans-Bold.ttf b/ui-components/src/main/res/font/noto_sans_bold.ttf similarity index 100% rename from desktop/src/jvmMain/resources/fonts/NotoSans-Bold.ttf rename to ui-components/src/main/res/font/noto_sans_bold.ttf diff --git a/desktop/src/jvmMain/resources/fonts/NotoSans-Medium.ttf b/ui-components/src/main/res/font/noto_sans_medium.ttf similarity index 100% rename from desktop/src/jvmMain/resources/fonts/NotoSans-Medium.ttf rename to ui-components/src/main/res/font/noto_sans_medium.ttf diff --git a/desktop/src/jvmMain/resources/fonts/NotoSans-Regular.ttf b/ui-components/src/main/res/font/noto_sans_regular.ttf similarity index 100% rename from desktop/src/jvmMain/resources/fonts/NotoSans-Regular.ttf rename to ui-components/src/main/res/font/noto_sans_regular.ttf diff --git a/desktop/src/jvmMain/resources/fonts/NotoSans-SemiBold.ttf b/ui-components/src/main/res/font/noto_sans_semibold.ttf similarity index 100% rename from desktop/src/jvmMain/resources/fonts/NotoSans-SemiBold.ttf rename to ui-components/src/main/res/font/noto_sans_semibold.ttf